diff --git a/.editorconfig b/.editorconfig index 33e50f9..144aaf5 100644 --- a/.editorconfig +++ b/.editorconfig @@ -42,10 +42,6 @@ dotnet_naming_style.pascal_case.required_suffix = dotnet_naming_style.pascal_case.word_separator = dotnet_naming_style.pascal_case.capitalization = pascal_case -dotnet_naming_style.pascal_case.required_prefix = -dotnet_naming_style.pascal_case.required_suffix = -dotnet_naming_style.pascal_case.word_separator = -dotnet_naming_style.pascal_case.capitalization = pascal_case dotnet_style_operator_placement_when_wrapping = beginning_of_line tab_width = 4 indent_size = 4 @@ -64,7 +60,6 @@ dotnet_style_prefer_inferred_tuple_names = true:suggestion dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion dotnet_style_prefer_compound_assignment = true:suggestion dotnet_style_prefer_simplified_interpolation = true:suggestion -dotnet_style_namespace_match_folder = true:suggestion indent_style = space [*.cs] @@ -74,8 +69,6 @@ csharp_using_directive_placement = outside_namespace:silent csharp_prefer_simple_using_statement = true:suggestion csharp_prefer_braces = true:silent csharp_style_namespace_declarations = file_scoped:suggestion -csharp_style_prefer_method_group_conversion = true:silent -csharp_style_prefer_top_level_statements = true:silent csharp_style_expression_bodied_methods = false:silent csharp_style_expression_bodied_constructors = false:silent csharp_style_expression_bodied_operators = false:silent @@ -85,13 +78,9 @@ csharp_style_expression_bodied_accessors = true:silent csharp_style_expression_bodied_lambdas = true:silent csharp_style_expression_bodied_local_functions = false:silent csharp_style_throw_expression = true:suggestion -csharp_style_prefer_null_check_over_type_check = true:suggestion csharp_prefer_simple_default_expression = true:suggestion -csharp_style_prefer_local_over_anonymous_function = true:suggestion csharp_style_prefer_index_operator = true:suggestion csharp_style_prefer_range_operator = true:suggestion -csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion -csharp_style_prefer_tuple_swap = true:suggestion csharp_style_prefer_utf8_string_literals = true:suggestion csharp_style_inlined_variable_declaration = true:suggestion csharp_style_deconstructed_variable_declaration = true:suggestion diff --git a/.gitignore b/.gitignore index 6b59843..ff2f6eb 100644 --- a/.gitignore +++ b/.gitignore @@ -402,3 +402,6 @@ data/ # Fonts, as we don't have license to distribute them Assets/Fonts/ + +# Mac DS_Store files +.DS_Store \ No newline at end of file diff --git a/Controllers/LockerImageController.cs b/Controllers/LockerImageController.cs index 0280924..4c25e99 100644 --- a/Controllers/LockerImageController.cs +++ b/Controllers/LockerImageController.cs @@ -12,7 +12,8 @@ namespace EasyFortniteStats_ImageApi.Controllers; public class AccountImageController( IHttpClientFactory clientFactory, AsyncKeyedLocker namedLock, - SharedAssets assets) + SharedAssets assets, + ILogger logger) : ControllerBase { private const string BASE_ITEM_IMAGE_PATH = "data/images/locker/items"; @@ -36,8 +37,9 @@ public class AccountImageController( [HttpPost] public async Task Post(Locker locker, [FromQuery] bool? lossless) { - Console.WriteLine( - $"Locker image request | Name = {locker.PlayerName} | Locale = {locker.Locale} | Items = {locker.Items.Length}"); + logger.LogInformation( + "Locker image request received | Name = {PlayerName} | Locale = {Locale} | Items = {Items}", + locker.PlayerName, locker.Locale, locker.Items.Length); var lockKey = $"locker_{locker.RequestId}"; using (await namedLock.LockAsync(lockKey).ConfigureAwait(false)) { @@ -79,11 +81,12 @@ private async Task GenerateImage(Locker locker) canvas.DrawRect(0, 0, imageInfo.Width, imageInfo.Height, backgroundPaint); + var textBounds = new SKRect(); var segoeFont = await assets.GetFont("Assets/Fonts/Segoe.ttf"); // don't dispose var icon = await assets.GetBitmap("Assets/Images/Locker/Icon.png"); // don't dispose var resize = (int)(50 * uiResizingFactor); - using var resizeIcon = icon!.Resize(new SKImageInfo(resize, resize), SKSamplingOptions.Default); + using var resizeIcon = icon!.Resize(new SKImageInfo(resize, resize), SKFilterQuality.High); canvas.DrawBitmap(resizeIcon, 50, 50); using var splitPaint = new SKPaint(); @@ -96,12 +99,14 @@ private async Task GenerateImage(Locker locker) splitPaint); using var namePaint = new SKPaint(); - using var nameFont = new SKFont(segoeFont, nameFontSize); namePaint.IsAntialias = true; namePaint.Color = SKColors.White; + namePaint.Typeface = segoeFont; + namePaint.TextSize = nameFontSize; + namePaint.FilterQuality = SKFilterQuality.Medium; - nameFont.MeasureText(locker.PlayerName, out var textBounds); - canvas.DrawText(locker.PlayerName, 50 + resizeIcon.Width + splitWidth * 3, 58 - textBounds.Top, nameFont, namePaint); + namePaint.MeasureText(locker.PlayerName, ref textBounds); + canvas.DrawText(locker.PlayerName, 50 + resizeIcon.Width + splitWidth * 3, 58 - textBounds.Top, namePaint); using var discordBoxBitmap = await ImageUtils.GenerateDiscordBox(assets, locker.UserName, uiResizingFactor); canvas.DrawBitmap(discordBoxBitmap, imageInfo.Width - 50 - discordBoxBitmap.Width, 39); @@ -156,15 +161,17 @@ await Parallel.ForEachAsync(locker.Items, options, async (item, token) => } catch (HttpRequestException e2) { - Console.WriteLine( - $"Failed to download image with status {e2.StatusCode} for {item.Name} ({item.ImageUrl}) "); + logger.LogWarning( + "Failed to download image with status {StatusCode} for {Name} ({ImageUrl}) ", + e2.StatusCode, item.Name, item.ImageUrl); itemImageBytes = null; } } catch (HttpRequestException e) { - Console.WriteLine( - $"Failed to download image with status {e.StatusCode} for {item.Name} ({item.ImageUrl}) "); + logger.LogWarning( + "Failed to download image with status {StatusCode} for {Name} ({ImageUrl}) ", + e.StatusCode, item.Name, item.ImageUrl); itemImageBytes = null; } @@ -173,7 +180,7 @@ await Parallel.ForEachAsync(locker.Items, options, async (item, token) => var itemImageRaw = SKBitmap.Decode(itemImageBytes); if (itemImageRaw.Width != 256 || itemImageRaw.Height != 256) { - itemImage = itemImageRaw.Resize(new SKImageInfo(256, 256), SKSamplingOptions.Default); + itemImage = itemImageRaw.Resize(new SKImageInfo(256, 256), SKFilterQuality.Medium); } else { @@ -214,13 +221,17 @@ private async Task GenerateItemCard(LockerItem lockerItem, SKBitmap? i else { using var questionmarkPaint = new SKPaint(); - using var questionmarkFont = new SKFont(await assets.GetFont("Assets/Fonts/Fortnite-86Bold.otf"), 256.0f); questionmarkPaint.IsAntialias = true; questionmarkPaint.Color = SKColors.White; - - questionmarkFont.MeasureText("?", out var questionmarkTextBounds); - - canvas.DrawText("?", (float)bitmap.Width / 2, (float)bitmap.Height / 2 + questionmarkTextBounds.Height / 2, SKTextAlign.Center, questionmarkFont, questionmarkPaint); + questionmarkPaint.Typeface = await assets.GetFont("Assets/Fonts/Fortnite-86Bold.otf"); + questionmarkPaint.TextSize = 256.0f; + questionmarkPaint.TextAlign = SKTextAlign.Center; + + var questionmarkTextBounds = new SKRect(); + questionmarkPaint.MeasureText("?", ref questionmarkTextBounds); + + canvas.DrawText("?", (float)bitmap.Width / 2, (float)bitmap.Height / 2 + questionmarkTextBounds.Height / 2, + questionmarkPaint); } var typeIcon = lockerItem.SourceType != SourceType.Other @@ -238,31 +249,40 @@ private async Task GenerateItemCard(LockerItem lockerItem, SKBitmap? i var fortniteFont = await assets.GetFont("Assets/Fonts/Fortnite.ttf"); // don't dispose using var namePaint = new SKPaint(); - using var nameFont = new SKFont(fortniteFont, 18.0f); namePaint.IsAntialias = true; + namePaint.TextSize = 18.0f; namePaint.Color = SKColors.White; + namePaint.Typeface = fortniteFont; + namePaint.TextAlign = SKTextAlign.Center; - nameFont.MeasureText(lockerItem.Name, out var entryNameTextBounds); - canvas.DrawText(lockerItem.Name, (float)bitmap.Width / 2, bitmap.Height - 59 + entryNameTextBounds.Height, SKTextAlign.Center, nameFont, namePaint); + var entryNameTextBounds = new SKRect(); + namePaint.MeasureText(lockerItem.Name, ref entryNameTextBounds); + canvas.DrawText(lockerItem.Name, (float)bitmap.Width / 2, bitmap.Height - 59 + entryNameTextBounds.Height, + namePaint); using var descriptionPaint = new SKPaint(); - using var descriptionFont = new SKFont(fortniteFont, 15.0f); descriptionPaint.IsAntialias = true; + descriptionPaint.TextSize = 15.0f; descriptionPaint.Color = SKColor.Parse(lockerItem.RarityColor); + descriptionPaint.Typeface = fortniteFont; + descriptionPaint.TextAlign = SKTextAlign.Center; - descriptionFont.MeasureText(lockerItem.Description, out entryNameTextBounds); + descriptionPaint.MeasureText(lockerItem.Description, ref entryNameTextBounds); canvas.DrawText(lockerItem.Description, (float)bitmap.Width / 2, - bitmap.Height - 42 + entryNameTextBounds.Height, SKTextAlign.Center, descriptionFont, descriptionPaint); + bitmap.Height - 42 + entryNameTextBounds.Height, descriptionPaint); using var sourcePaint = new SKPaint(); - using var sourceFont = new SKFont(fortniteFont, 15.0f); sourcePaint.IsAntialias = true; + sourcePaint.TextSize = 15.0f; sourcePaint.Color = SKColors.White; + sourcePaint.Typeface = fortniteFont; + sourcePaint.TextAlign = SKTextAlign.Right; var fontOffset = lockerItem.SourceType == SourceType.Other ? 10 : 42; - sourceFont.MeasureText(lockerItem.Source, out entryNameTextBounds); - canvas.DrawText(lockerItem.Source, bitmap.Width - fontOffset, bitmap.Height - entryNameTextBounds.Height + 8, SKTextAlign.Right, sourceFont, sourcePaint); + sourcePaint.MeasureText(lockerItem.Source, ref entryNameTextBounds); + canvas.DrawText(lockerItem.Source, bitmap.Width - fontOffset, bitmap.Height - entryNameTextBounds.Height + 8, + sourcePaint); return bitmap; } @@ -272,14 +292,16 @@ private async Task GenerateFooter(float resizeFactor) var poppinsFont = await assets.GetFont("Assets/Fonts/Poppins.ttf"); // don't dispose using var textPaint = new SKPaint(); - using var textFont = new SKFont(poppinsFont, 40.0f * resizeFactor); textPaint.IsAntialias = true; + textPaint.TextSize = 40.0f * resizeFactor; textPaint.Color = SKColors.White; + textPaint.Typeface = poppinsFont; //var text = "EasyFnStats.com".ToUpper(); const string text = "EASYFNSTATS.COM"; - textFont.MeasureText(text, out var textBounds); + var textBounds = new SKRect(); + textPaint.MeasureText(text, ref textBounds); var imageInfo = new SKImageInfo((int)((50 + 10 + 5 + 10) * resizeFactor + textBounds.Width), (int)(50 * resizeFactor)); @@ -288,7 +310,7 @@ private async Task GenerateFooter(float resizeFactor) var logoBitmap = await assets.GetBitmap("Assets/Images/Logo.png"); // don't dispose var logoBitmapResize = - logoBitmap!.Resize(new SKImageInfo(imageInfo.Height, imageInfo.Height), SKSamplingOptions.Default); + logoBitmap!.Resize(new SKImageInfo(imageInfo.Height, imageInfo.Height), SKFilterQuality.High); canvas.DrawBitmap(logoBitmapResize, new SKPoint(0, 0)); var splitR = 3 * resizeFactor; @@ -299,7 +321,7 @@ private async Task GenerateFooter(float resizeFactor) canvas.DrawRoundRect((50 + 10) * resizeFactor, (imageInfo.Height - 40 * resizeFactor) / 2, 5 * resizeFactor, 40 * resizeFactor, splitR, splitR, splitPaint); - canvas.DrawText(text, (50 + 10 + 5 + 10) * resizeFactor, (imageInfo.Height + textBounds.Height) / 2, textFont, textPaint); + canvas.DrawText(text, (50 + 10 + 5 + 10) * resizeFactor, (imageInfo.Height + textBounds.Height) / 2, textPaint); return bitmap; } diff --git a/Controllers/ShopImageController.cs b/Controllers/ShopImageController.cs index 8cd7940..5ba41ba 100644 --- a/Controllers/ShopImageController.cs +++ b/Controllers/ShopImageController.cs @@ -15,7 +15,8 @@ public partial class ShopImageController( IMemoryCache cache, IHttpClientFactory clientFactory, AsyncKeyedLocker namedLock, - SharedAssets assets) + SharedAssets assets, + ILogger logger) : ControllerBase { // Constants @@ -55,7 +56,7 @@ public async Task Shop([FromBody] Shop shop, [FromQuery] string? { locale ??= "en"; var _isNewShop = isNewShop ?? false; - Console.WriteLine($"Item Shop image request | Locale = {locale} | New Shop = {_isNewShop}"); + logger.LogInformation("Item Shop image request received | Locale = {Locale} | New Shop = {IsNewShop}", locale, _isNewShop); // Hash the section ids var templateHash = string.Join('-', shop.Sections.Select(x => x.Id)).GetHashCode().ToString(); @@ -64,10 +65,12 @@ public async Task Shop([FromBody] Shop shop, [FromQuery] string? using (await namedLock.LockAsync("shop_template").ConfigureAwait(false)) { + logger.LogDebug("Acquired shop template lock"); templateBitmap = cache.Get($"shop_template_bmp_{templateHash}"); locationData = cache.Get($"shop_location_data_{templateHash}"); if (_isNewShop || templateBitmap is null) { + logger.LogDebug("Generating new shop template"); await PrefetchImages(shop); var templateGenerationResult = await GenerateTemplate(shop); templateBitmap = templateGenerationResult.Item2; @@ -75,6 +78,7 @@ public async Task Shop([FromBody] Shop shop, [FromQuery] string? cache.Set($"shop_template_bmp_{templateHash}", templateBitmap, ShopImageCacheOptions); cache.Set($"shop_location_data_{templateHash}", locationData, TimeSpan.FromMinutes(10)); } + logger.LogDebug("Releasing shop template lock"); } SKBitmap? localeTemplateBitmap; @@ -82,12 +86,15 @@ public async Task Shop([FromBody] Shop shop, [FromQuery] string? var lockName = $"shop_template_{locale}"; using (await namedLock.LockAsync(lockName).ConfigureAwait(false)) { + logger.LogDebug("Acquired locale shop template lock for locale {Locale}", locale); localeTemplateBitmap = cache.Get($"shop_template_{locale}_bmp"); if (_isNewShop || localeTemplateBitmap == null) { + logger.LogDebug("Generating new locale shop template for locale {Locale}", locale); localeTemplateBitmap = await GenerateLocaleTemplate(shop, templateBitmap, locationData!); cache.Set($"shop_template_{locale}_bmp", localeTemplateBitmap, ShopImageCacheOptions); } + logger.LogDebug("Releasing locale shop template lock for locale {Locale}", locale); } using var localeTemplateBitmapCopy = localeTemplateBitmap.Copy(); @@ -102,7 +109,7 @@ public async Task ShopSection([FromBody] ShopSection section, [Fr { locale ??= "en"; var _isNewShop = isNewShop ?? false; - Console.WriteLine($"Item Shop section image request | Locale = {locale} | New Shop = {section.Id}"); + logger.LogInformation("Item Shop section image request received | Locale = {Locale} | New Shop = {SectionId}", locale, section.Id); SKBitmap? templateBitmap; ShopSectionLocationData? shopSectionLocationData; @@ -214,8 +221,9 @@ private async Task GenerateShopImage(Shop shop, SKBitmap templateBitma { using var backgroundImagePaint = new SKPaint(); backgroundImagePaint.IsAntialias = true; + backgroundImagePaint.FilterQuality = SKFilterQuality.Medium; - using var resizedCustomBackgroundBitmap = backgroundBitmap.Resize(imageInfo, SKSamplingOptions.Default); + using var resizedCustomBackgroundBitmap = backgroundBitmap.Resize(imageInfo, SKFilterQuality.Medium); backgroundImagePaint.Shader = SKShader.CreateBitmap(resizedCustomBackgroundBitmap, SKShaderTileMode.Clamp, SKShaderTileMode.Repeat); @@ -227,10 +235,13 @@ private async Task GenerateShopImage(Shop shop, SKBitmap templateBitma if (shop is { CreatorCode: not null, CreatorCodeTitle: not null }) { using var shopTitlePaint = new SKPaint(); - using var shopTitleFont = new SKFont(await assets.GetFont("Assets/Fonts/Fortnite-86Bold.otf"), TITLE_FONT_SIZE); shopTitlePaint.IsAntialias = true; + shopTitlePaint.TextSize = TITLE_FONT_SIZE; + shopTitlePaint.Typeface = await assets.GetFont("Assets/Fonts/Fortnite-86Bold.otf"); + + + var shopTitleWidth = shopTitlePaint.MeasureText(shop.Title); - var shopTitleWidth = shopTitleFont.MeasureText(shop.Title); var maxBoxWidth = imageInfo.Width - 3 * HORIZONTAL_PADDING - shopTitleWidth; using var creatorCodeBoxBitmap = @@ -256,22 +267,27 @@ private async Task GenerateLocaleTemplate(Shop shop, SKBitmap template // Drawing the shop title using var shopTitlePaint = new SKPaint(); - using var shopTitleFont = new SKFont(await assets.GetFont("Assets/Fonts/Fortnite-86Bold.otf"), TITLE_FONT_SIZE); shopTitlePaint.IsAntialias = true; + shopTitlePaint.TextSize = TITLE_FONT_SIZE; shopTitlePaint.Color = SKColors.White; + shopTitlePaint.Typeface = await assets.GetFont("Assets/Fonts/Fortnite-86Bold.otf"); - var shopTitleWidth = shopTitleFont.MeasureText(shop.Title); - canvas.DrawText(shop.Title, 100, 50 - shopTitleFont.Metrics.Ascent, shopTitleFont, shopTitlePaint); + var shopTitleWidth = shopTitlePaint.MeasureText(shop.Title); + canvas.DrawText(shop.Title, 100, 50 - shopTitlePaint.FontMetrics.Ascent, shopTitlePaint); // Drawing the date using var datePaint = new SKPaint(); - using var dateFont = new SKFont(await assets.GetFont("Assets/Fonts/Fortnite-86BoldItalic.otf"), DATE_FONT_SIZE); datePaint.IsAntialias = true; + datePaint.TextSize = DATE_FONT_SIZE; datePaint.Color = SKColors.White; + datePaint.Typeface = await assets.GetFont("Assets/Fonts/Fortnite-86BoldItalic.otf"); + datePaint.TextAlign = SKTextAlign.Center; + var datePoint = new SKPoint( - Math.Max(HORIZONTAL_PADDING + shopTitleWidth / 2f, HORIZONTAL_PADDING + dateFont.MeasureText(shop.Date) / 2), - 313 - dateFont.Metrics.Ascent); - canvas.DrawText(shop.Date, datePoint, SKTextAlign.Center, dateFont, datePaint); + Math.Max(HORIZONTAL_PADDING + shopTitleWidth / 2f, + HORIZONTAL_PADDING + datePaint.MeasureText(shop.Date) / 2), + 313 - datePaint.FontMetrics.Ascent); + canvas.DrawText(shop.Date, datePoint, datePaint); foreach (var sectionLocationData in shopSectionLocationData) { @@ -281,13 +297,14 @@ private async Task GenerateLocaleTemplate(Shop shop, SKBitmap template if (sectionLocationData.Name != null) { using var sectionNamePaint = new SKPaint(); - using var sectionNameFont = new SKFont(await assets.GetFont("Assets/Fonts/Fortnite-86BoldItalic.otf"), SECTION_NAME_FONT_SIZE); sectionNamePaint.IsAntialias = true; + sectionNamePaint.TextSize = SECTION_NAME_FONT_SIZE; sectionNamePaint.Color = SKColors.White; + sectionNamePaint.Typeface = await assets.GetFont("Assets/Fonts/Fortnite-86BoldItalic.otf"); var sectionNamePoint = new SKPoint(sectionLocationData.Name.X, - sectionLocationData.Name.Y - sectionNameFont.Metrics.Ascent); - canvas.DrawText(shopSection?.Name, sectionNamePoint, sectionNameFont, sectionNamePaint); + sectionLocationData.Name.Y - sectionNamePaint.FontMetrics.Ascent); + canvas.DrawText(shopSection?.Name, sectionNamePoint, sectionNamePaint); } foreach (var entryLocationData in sectionLocationData.Entries) @@ -297,43 +314,49 @@ private async Task GenerateLocaleTemplate(Shop shop, SKBitmap template continue; using var entryNamePaint = new SKPaint(); - using var entryNameFont = new SKFont(await assets.GetFont("Assets/Fonts/Fortnite-75Medium.otf"), ENTRY_NAME_FONT_SIZE); + entryNamePaint.IsAntialias = true; + entryNamePaint.TextSize = ENTRY_NAME_FONT_SIZE; entryNamePaint.Color = SKColors.White; + entryNamePaint.Typeface = await assets.GetFont("Assets/Fonts/Fortnite-75Medium.otf"); - SKRect entryNameTextBounds; - var nameLines = SplitNameText(shopEntry.Name, entryLocationData.Name.MaxWidth ?? 0, entryNameFont, entryNamePaint); + var entryNameTextBounds = new SKRect(); + var nameLines = SplitNameText(shopEntry.Name, entryLocationData.Name.MaxWidth ?? 0, entryNamePaint); if (nameLines.Length > 1) { - entryNameFont.MeasureText(nameLines[0], out entryNameTextBounds, entryNamePaint); - canvas.DrawText(nameLines[0], entryLocationData.Name.X, entryLocationData.Name.Y + entryNameTextBounds.Height - 33, entryNameFont, entryNamePaint); + entryNamePaint.MeasureText(nameLines[0], ref entryNameTextBounds); + canvas.DrawText(nameLines[0], entryLocationData.Name.X, + entryLocationData.Name.Y + entryNameTextBounds.Height - 33, entryNamePaint); } - entryNameFont.MeasureText(nameLines.Last(), out entryNameTextBounds, entryNamePaint); - canvas.DrawText(nameLines.Last(), entryLocationData.Name.X, entryLocationData.Name.Y + entryNameTextBounds.Height, entryNameFont, entryNamePaint); + entryNamePaint.MeasureText(nameLines.Last(), ref entryNameTextBounds); + canvas.DrawText(nameLines.Last(), entryLocationData.Name.X, + entryLocationData.Name.Y + entryNameTextBounds.Height, entryNamePaint); // Draw the shop entry price using var pricePaint = new SKPaint(); - using var priceFont = new SKFont(await assets.GetFont("Assets/Fonts/Fortnite-75Medium.otf"), ENTRY_PRICE_FONT_SIZE); pricePaint.IsAntialias = true; + pricePaint.TextSize = ENTRY_PRICE_FONT_SIZE; pricePaint.Color = SKColors.White; + pricePaint.Typeface = await assets.GetFont("Assets/Fonts/Fortnite-75Medium.otf"); - var priceTextWidth = priceFont.MeasureText(shopEntry.FinalPrice); + var priceTextWidth = pricePaint.MeasureText(shopEntry.FinalPrice); var pricePoint = new SKPoint(entryLocationData.Price.X, - entryLocationData.Price.Y - priceFont.Metrics.Descent); - canvas.DrawText(shopEntry.FinalPrice, pricePoint, priceFont, pricePaint); + entryLocationData.Price.Y - pricePaint.FontMetrics.Descent); + canvas.DrawText(shopEntry.FinalPrice, pricePoint, pricePaint); // Draw strikeout old price if item is discounted if (shopEntry.FinalPrice != shopEntry.RegularPrice) { using var oldPricePaint = new SKPaint(); - using var oldPriceFont = new SKFont(await assets.GetFont("Assets/Fonts/Fortnite-75Medium.otf"), ENTRY_PRICE_FONT_SIZE); oldPricePaint.IsAntialias = true; + oldPricePaint.TextSize = ENTRY_PRICE_FONT_SIZE; oldPricePaint.Color = SKColors.White.WithAlpha((int)(.6 * 255)); + oldPricePaint.Typeface = await assets.GetFont("Assets/Fonts/Fortnite-75Medium.otf"); - var oldPriceTextWidth = oldPriceFont.MeasureText(shopEntry.RegularPrice); + var oldPriceTextWidth = oldPricePaint.MeasureText(shopEntry.RegularPrice); var oldPricePoint = new SKPoint(entryLocationData.Price.X + priceTextWidth + 9, - entryLocationData.Price.Y - priceFont.Metrics.Descent); - canvas.DrawText(shopEntry.RegularPrice, oldPricePoint, oldPriceFont, oldPricePaint); + entryLocationData.Price.Y - pricePaint.FontMetrics.Descent); + canvas.DrawText(shopEntry.RegularPrice, oldPricePoint, oldPricePaint); // Draw the strikeout line using var strikePaint = new SKPaint(); @@ -454,21 +477,26 @@ private async Task GenerateCreatorCodeBox(string creatorCodeTitle, str creatorCode = $"{creatorCode} "; using var creatorCodeTitlePaint = new SKPaint(); - using var creatorCodeTitleFont = new SKFont(await assets.GetFont("Assets/Fonts/Fortnite-76Bold.otf"), 100f); creatorCodeTitlePaint.IsAntialias = true; + creatorCodeTitlePaint.TextSize = 100f; + creatorCodeTitlePaint.Typeface = await assets.GetFont("Assets/Fonts/Fortnite-76Bold.otf"); creatorCodeTitlePaint.Color = SKColors.Black; using var creatorCodePaint = new SKPaint(); - using var creatorCodeFont = new SKFont(await assets.GetFont("Assets/Fonts/Fortnite-76Bold.otf"), 100f); creatorCodePaint.IsAntialias = true; + creatorCodePaint.TextSize = 100f; + creatorCodePaint.Typeface = await assets.GetFont("Assets/Fonts/Fortnite-76Bold.otf"); creatorCodePaint.Color = new SKColor(178, 165, 255); + creatorCodePaint.TextAlign = SKTextAlign.Right; - float width = creatorCodeTitleFont.MeasureText(creatorCodeTitle) + creatorCodeTitleFont.MeasureText(creatorCode), height = 150f; + float width = + creatorCodeTitlePaint.MeasureText(creatorCodeTitle) + creatorCodeTitlePaint.MeasureText(creatorCode), + height = 150f; while (width > maxWidth) { - creatorCodeTitleFont.Size--; - creatorCodeFont.Size--; - width = creatorCodeTitleFont.MeasureText(creatorCodeTitle) + creatorCodeFont.MeasureText(creatorCode); + creatorCodeTitlePaint.TextSize--; + creatorCodePaint.TextSize--; + width = creatorCodeTitlePaint.MeasureText(creatorCodeTitle) + creatorCodePaint.MeasureText(creatorCode); height--; } @@ -482,10 +510,10 @@ private async Task GenerateCreatorCodeBox(string creatorCodeTitle, str boxPaint.Style = SKPaintStyle.Fill; canvas.DrawRoundRect(new SKRect(0, 0, imageInfo.Width, imageInfo.Height), 100, 100, boxPaint); - var y = (imageInfo.Height - creatorCodeFont.Spacing) / 2 - creatorCodeFont.Metrics.Ascent; + var y = (imageInfo.Height - creatorCodePaint.FontSpacing) / 2 - creatorCodePaint.FontMetrics.Ascent; - canvas.DrawText(creatorCodeTitle, 0, y, creatorCodeTitleFont, creatorCodeTitlePaint); - canvas.DrawText(creatorCode, imageInfo.Width, y, SKTextAlign.Right, creatorCodeFont, creatorCodePaint); + canvas.DrawText(creatorCodeTitle, 0, y, creatorCodeTitlePaint); + canvas.DrawText(creatorCode, imageInfo.Width, y, creatorCodePaint); return bitmap; } @@ -493,11 +521,13 @@ private async Task GenerateCreatorCodeBox(string creatorCodeTitle, str private async Task GenerateBanner(string text, IReadOnlyList colors, int maxWidth) { using var bannerPaint = new SKPaint(); - using var bannerFont = new SKFont(await assets.GetFont("Assets/Fonts/Fortnite-76BoldItalic.otf"), 17.0f); bannerPaint.IsAntialias = true; + bannerPaint.TextSize = 17.0f; + bannerPaint.Typeface = await assets.GetFont("Assets/Fonts/Fortnite-76BoldItalic.otf"); bannerPaint.Color = SKColor.Parse(colors[1]); - bannerFont.MeasureText(text, out var textBounds, bannerPaint); + var textBounds = new SKRect(); + bannerPaint.MeasureText(text, ref textBounds); var maxTextWidth = maxWidth - 2 * 13; var imageInfo = new SKImageInfo(Math.Min(2 * 13 + (int)textBounds.Width, maxWidth), 34); @@ -516,7 +546,7 @@ private async Task GenerateBanner(string text, IReadOnlyList c while (textBounds.Width > maxTextWidth) { text = text.Remove(text.Length - 1, 1); - bannerFont.MeasureText(text + "...", out textBounds); + bannerPaint.MeasureText(text + "...", ref textBounds); } text += "..."; @@ -524,7 +554,7 @@ private async Task GenerateBanner(string text, IReadOnlyList c // 6 + textBounds.Top - canvas.DrawText(text, 13, (float)imageInfo.Height / 2 - textBounds.MidY, bannerFont, bannerPaint); + canvas.DrawText(text, 13, (float)imageInfo.Height / 2 - textBounds.MidY, bannerPaint); return bitmap; } @@ -600,7 +630,7 @@ private async Task GenerateItemCard(ShopEntry shopEntry) // Scale image down to fit the card if (shopEntry is { ImageType: "track", ImageUrl: null }) { - using var coverBitmap = shopEntry.Image.Resize(new SKImageInfo(236, 236), SKSamplingOptions.Default); + using var coverBitmap = shopEntry.Image.Resize(new SKImageInfo(236, 236), SKFilterQuality.Medium); using var roundedCoverBitmap = new SKBitmap(236, 236); using var roundedCoverCanvas = new SKCanvas(roundedCoverBitmap); @@ -627,8 +657,8 @@ private async Task GenerateItemCard(ShopEntry shopEntry) } using var resizedImageBitmap = - shopEntry.Image.Resize(new SKImageInfo(resizeWidth, resizeHeight), SKSamplingOptions.Default) ?? - shopEntry.Image; + shopEntry.Image.Resize(new SKImageInfo(resizeWidth, resizeHeight), SKFilterQuality.Medium) ?? + shopEntry.Image.Copy(); // Car bundles get centered in the middle of the card vertically if (shopEntry.ImageType == "car-bundle") @@ -697,25 +727,29 @@ private async Task GenerateItemCard(ShopEntry shopEntry) using var paint = new SKPaint(); using var font = new SKFont(await assets.GetFont("Assets/Fonts/Fortnite-74Regular.otf"), 35.0f); paint.IsAntialias = true; + paint.TextSize = 35.0f; paint.Color = SKColors.White; + paint.Typeface = await assets.GetFont("Assets/Fonts/Fortnite-74Regular.otf"); + paint.TextAlign = SKTextAlign.Right; - canvas.DrawText("+", imageInfo.Width - 18, imageInfo.Height - font.Metrics.Descent + 3, SKTextAlign.Right, font, paint); + canvas.DrawText("+", imageInfo.Width - 18, imageInfo.Height - paint.FontMetrics.Descent + 3, paint); } return bitmap; } - private static string[] SplitNameText(string text, int maxWidth, SKFont font, SKPaint paint) + private static string[] SplitNameText(string text, int maxWidth, SKPaint paint) { var regex = NameSplitRegex(); var matches = regex.Matches(text); - + var currentLine = 0; var lines = new StringBuilder[] { new(), new() }; foreach (Match match in matches) { var line = lines[currentLine]; - font.MeasureText(line + match.Value, out var bounds, paint); + var bounds = new SKRect(); + paint.MeasureText(line + match.Value, ref bounds); if (bounds.Width > maxWidth) currentLine++; if (currentLine >= 2) { @@ -729,13 +763,14 @@ private static string[] SplitNameText(string text, int maxWidth, SKFont font, SK // Adjust lines that are too long and add ellipsis foreach (var line in lines) { - font.MeasureText(line.ToString(), out var textBounds, paint); + var textBounds = new SKRect(); + paint.MeasureText(line.ToString(), ref textBounds); if (textBounds.Width <= maxWidth) continue; while (textBounds.Width > maxWidth) { line.Remove(line.Length - 1, 1); - font.MeasureText(line + "...", out textBounds); + paint.MeasureText(line + "...", ref textBounds); } line.Append("..."); diff --git a/Controllers/StatsImageController.cs b/Controllers/StatsImageController.cs index cc3595c..0c0f55f 100644 --- a/Controllers/StatsImageController.cs +++ b/Controllers/StatsImageController.cs @@ -9,13 +9,13 @@ namespace EasyFortniteStats_ImageApi.Controllers; [ApiController] [Route("stats")] -public class StatsImageController(IMemoryCache cache, AsyncKeyedLocker namedLock, SharedAssets assets) +public class StatsImageController(IMemoryCache cache, AsyncKeyedLocker namedLock, SharedAssets assets, ILogger logger) : ControllerBase { [HttpPost] public async Task Post(Stats stats, StatsType type = StatsType.Normal) { - Console.WriteLine($"Stats image request | Name = {stats.PlayerName} | Type = {type}"); + logger.LogInformation("Stats image request received | Name = {PlayerName} | Type = {Type}", stats.PlayerName, type); if (type == StatsType.Normal && stats.Teams == null) return BadRequest("Normal stats type requested but no team stats were provided."); if (type == StatsType.Competitive && stats.Competitive == null) @@ -67,11 +67,12 @@ await assets.GetBitmap("data/images/{0}", { using var backgroundImagePaint = new SKPaint(); backgroundImagePaint.IsAntialias = true; + backgroundImagePaint.FilterQuality = SKFilterQuality.Medium; if (customBackgroundBitmap.Width != imageInfo.Width || customBackgroundBitmap.Height != imageInfo.Height) { using var resizedCustomBackgroundBitmap = - customBackgroundBitmap.Resize(imageInfo, SKSamplingOptions.Default); + customBackgroundBitmap.Resize(imageInfo, SKFilterQuality.Medium); backgroundImagePaint.Shader = SKShader.CreateBitmap(resizedCustomBackgroundBitmap, SKShaderTileMode.Clamp, SKShaderTileMode.Repeat); } @@ -90,27 +91,30 @@ await assets.GetBitmap("data/images/{0}", using var boxPaint = new SKPaint(); boxPaint.IsAntialias = true; - boxPaint.Color = SKColors.White.WithAlpha((int) (.2 * 255)); + boxPaint.Color = SKColors.White.WithAlpha((int)(.2 * 255)); var fortniteFont = await assets.GetFont("Assets/Fonts/Fortnite.ttf"); // don't dispose var segoeFont = await assets.GetFont("Assets/Fonts/Segoe.ttf"); // don't dispose using var competitiveBoxTitlePaint = new SKPaint(); - using var competitiveBoxTitleFont = new SKFont(fortniteFont, 25); competitiveBoxTitlePaint.IsAntialias = true; competitiveBoxTitlePaint.Color = SKColors.White; + competitiveBoxTitlePaint.Typeface = fortniteFont; + competitiveBoxTitlePaint.TextSize = 25; using var boxTitlePaint = new SKPaint(); - using var boxTitleFont = new SKFont(fortniteFont, 50); boxTitlePaint.IsAntialias = true; boxTitlePaint.Color = SKColors.White; + boxTitlePaint.Typeface = fortniteFont; + boxTitlePaint.TextSize = 50; using var titlePaint = new SKPaint(); - using var titleFont = new SKFont(segoeFont, 20); titlePaint.IsAntialias = true; titlePaint.Color = SKColors.LightGray; + titlePaint.Typeface = segoeFont; + titlePaint.TextSize = 20; - SKRect textBounds; + var textBounds = new SKRect(); if (type == StatsType.Competitive) { @@ -120,7 +124,7 @@ await assets.GetBitmap("data/images/{0}", using var overlayBoxPaint = new SKPaint(); overlayBoxPaint.IsAntialias = true; - overlayBoxPaint.Color = SKColors.White.WithAlpha((int) (.2 * 255)); + overlayBoxPaint.Color = SKColors.White.WithAlpha((int)(.2 * 255)); var upperBoxRect = SKRect.Create(49, 159, 437, 158); var upperBox = new SKRoundRect(upperBoxRect); @@ -130,7 +134,7 @@ await assets.GetBitmap("data/images/{0}", using var splitPaint = new SKPaint(); splitPaint.IsAntialias = true; - splitPaint.Color = SKColors.White.WithAlpha((int) (.5 * 255)); + splitPaint.Color = SKColors.White.WithAlpha((int)(.5 * 255)); canvas.DrawRoundRect(267, 192, 1, 77, 1, 1, splitPaint); var buildLogo = await assets.GetBitmap("Assets/Images/Stats/BuildLogo.png"); // don't dispose @@ -139,29 +143,29 @@ await assets.GetBitmap("data/images/{0}", var zeroBuildLogo = await assets.GetBitmap("Assets/Images/Stats/ZeroBuildLogo.png"); // don't dispose canvas.DrawBitmap(zeroBuildLogo, new SKPoint(317, 277)); - competitiveBoxTitleFont.MeasureText("OVERALL", out textBounds); - canvas.DrawText("OVERALL", 211, 305 - textBounds.Top, competitiveBoxTitleFont, competitiveBoxTitlePaint); + competitiveBoxTitlePaint.MeasureText("OVERALL", ref textBounds); + canvas.DrawText("OVERALL", 211, 305 - textBounds.Top, competitiveBoxTitlePaint); - titleFont.MeasureText("Earnings", out textBounds); - canvas.DrawText("Earnings", 70, 338 - textBounds.Top, titleFont, titlePaint); + titlePaint.MeasureText("Earnings", ref textBounds); + canvas.DrawText("Earnings", 70, 338 - textBounds.Top, titlePaint); - titleFont.MeasureText("Power Ranking", out textBounds); - canvas.DrawText("Power Ranking", 250, 338 - textBounds.Top, titleFont, titlePaint); + titlePaint.MeasureText("Power Ranking", ref textBounds); + canvas.DrawText("Power Ranking", 250, 338 - textBounds.Top, titlePaint); - titleFont.MeasureText("Games", out textBounds); - canvas.DrawText("Games", 70, 414 - textBounds.Top, titleFont, titlePaint); + titlePaint.MeasureText("Games", ref textBounds); + canvas.DrawText("Games", 70, 414 - textBounds.Top, titlePaint); - titleFont.MeasureText("Wins", out textBounds); - canvas.DrawText("Wins", 231, 414 - textBounds.Top, titleFont, titlePaint); + titlePaint.MeasureText("Wins", ref textBounds); + canvas.DrawText("Wins", 231, 414 - textBounds.Top, titlePaint); - titleFont.MeasureText("Win%", out textBounds); - canvas.DrawText("Win%", 370, 414 - textBounds.Top, titleFont, titlePaint); + titlePaint.MeasureText("Win%", ref textBounds); + canvas.DrawText("Win%", 370, 414 - textBounds.Top, titlePaint); - titleFont.MeasureText("Kills", out textBounds); - canvas.DrawText("Kills", 70, 491 - textBounds.Top, titleFont, titlePaint); + titlePaint.MeasureText("Kills", ref textBounds); + canvas.DrawText("Kills", 70, 491 - textBounds.Top, titlePaint); - titleFont.MeasureText("K/D", out textBounds); - canvas.DrawText("K/D", 231, 491 - textBounds.Top, titleFont, titlePaint); + titlePaint.MeasureText("K/D", ref textBounds); + canvas.DrawText("K/D", 231, 491 - textBounds.Top, titlePaint); } else { @@ -169,42 +173,42 @@ await assets.GetBitmap("data/images/{0}", DrawBlurredRoundRect(bitmap, overallBoxRect); canvas.DrawRoundRect(overallBoxRect, boxPaint); - boxTitleFont.MeasureText("OVERALL", out textBounds); - canvas.DrawText("OVERALL", 60, 134 - textBounds.Top, boxTitleFont, boxTitlePaint); + boxTitlePaint.MeasureText("OVERALL", ref textBounds); + canvas.DrawText("OVERALL", 60, 134 - textBounds.Top, boxTitlePaint); - titleFont.MeasureText("Games", out textBounds); - canvas.DrawText("Games", 70, 184 - textBounds.Top, titleFont, titlePaint); + titlePaint.MeasureText("Games", ref textBounds); + canvas.DrawText("Games", 70, 184 - textBounds.Top, titlePaint); - titleFont.MeasureText("Wins", out textBounds); - canvas.DrawText("Wins", 231, 184 - textBounds.Top, titleFont, titlePaint); + titlePaint.MeasureText("Wins", ref textBounds); + canvas.DrawText("Wins", 231, 184 - textBounds.Top, titlePaint); - titleFont.MeasureText("Win%", out textBounds); - canvas.DrawText("Win%", 370, 184 - textBounds.Top, titleFont, titlePaint); + titlePaint.MeasureText("Win%", ref textBounds); + canvas.DrawText("Win%", 370, 184 - textBounds.Top, titlePaint); - titleFont.MeasureText("Kills", out textBounds); - canvas.DrawText("Kills", 70, 261 - textBounds.Top, titleFont, titlePaint); + titlePaint.MeasureText("Kills", ref textBounds); + canvas.DrawText("Kills", 70, 261 - textBounds.Top, titlePaint); - titleFont.MeasureText("K/D", out textBounds); - canvas.DrawText("K/D", 231, 261 - textBounds.Top, titleFont, titlePaint); + titlePaint.MeasureText("K/D", ref textBounds); + canvas.DrawText("K/D", 231, 261 - textBounds.Top, titlePaint); - titleFont.MeasureText("Playtime since Season 7", out textBounds); - canvas.DrawText("Playtime since Season 7", 70, 338 - textBounds.Top, titleFont, titlePaint); + titlePaint.MeasureText("Playtime since Season 7", ref textBounds); + canvas.DrawText("Playtime since Season 7", 70, 338 - textBounds.Top, titlePaint); - titleFont.MeasureText("days", out textBounds); - canvas.DrawText("days", 70, 397 - textBounds.Top, titleFont, titlePaint); + titlePaint.MeasureText("days", ref textBounds); + canvas.DrawText("days", 70, 397 - textBounds.Top, titlePaint); - titleFont.MeasureText("hours", out textBounds); - canvas.DrawText("hours", 147, 397 - textBounds.Top, titleFont, titlePaint); + titlePaint.MeasureText("hours", ref textBounds); + canvas.DrawText("hours", 147, 397 - textBounds.Top, titlePaint); - titleFont.MeasureText("minutes", out textBounds); - canvas.DrawText("minutes", 231, 397 - textBounds.Top, titleFont, titlePaint); + titlePaint.MeasureText("minutes", ref textBounds); + canvas.DrawText("minutes", 231, 397 - textBounds.Top, titlePaint); - titleFont.MeasureText("BattlePass Level", out textBounds); - canvas.DrawText("BattlePass Level", 70, 442 - textBounds.Top, titleFont, titlePaint); + titlePaint.MeasureText("BattlePass Level", ref textBounds); + canvas.DrawText("BattlePass Level", 70, 442 - textBounds.Top, titlePaint); using var battlePassBarBackgroundPaint = new SKPaint(); battlePassBarBackgroundPaint.IsAntialias = true; - battlePassBarBackgroundPaint.Color = SKColors.White.WithAlpha((int) (.3 * 255)); + battlePassBarBackgroundPaint.Color = SKColors.White.WithAlpha((int)(.3 * 255)); canvas.DrawRoundRect(158, 483, 309, 20, 10, 10, battlePassBarBackgroundPaint); } @@ -213,116 +217,116 @@ await assets.GetBitmap("data/images/{0}", DrawBlurredRoundRect(bitmap, soloBoxRect); canvas.DrawRoundRect(soloBoxRect, boxPaint); - boxTitleFont.MeasureText("SOLO", out textBounds); - canvas.DrawText("SOLO", 527, 134 - textBounds.Top, boxTitleFont, boxTitlePaint); + boxTitlePaint.MeasureText("SOLO", ref textBounds); + canvas.DrawText("SOLO", 527, 134 - textBounds.Top, boxTitlePaint); var soloIcon = await assets.GetBitmap("Assets/Images/Stats/PlaylistIcons/solo.png"); // don't dispose canvas.DrawBitmap(soloIcon, new SKPoint(648, 134)); - titleFont.MeasureText("Games", out textBounds); - canvas.DrawText("Games", 537, 184 - textBounds.Top, titleFont, titlePaint); + titlePaint.MeasureText("Games", ref textBounds); + canvas.DrawText("Games", 537, 184 - textBounds.Top, titlePaint); - titleFont.MeasureText("Wins", out textBounds); - canvas.DrawText("Wins", 698, 184 - textBounds.Top, titleFont, titlePaint); + titlePaint.MeasureText("Wins", ref textBounds); + canvas.DrawText("Wins", 698, 184 - textBounds.Top, titlePaint); - titleFont.MeasureText("Win%", out textBounds); - canvas.DrawText("Win%", 837, 184 - textBounds.Top, titleFont, titlePaint); + titlePaint.MeasureText("Win%", ref textBounds); + canvas.DrawText("Win%", 837, 184 - textBounds.Top, titlePaint); - titleFont.MeasureText("Kills", out textBounds); - canvas.DrawText("Kills", 537, 261 - textBounds.Top, titleFont, titlePaint); + titlePaint.MeasureText("Kills", ref textBounds); + canvas.DrawText("Kills", 537, 261 - textBounds.Top, titlePaint); - titleFont.MeasureText("K/D", out textBounds); - canvas.DrawText("K/D", 698, 261 - textBounds.Top, titleFont, titlePaint); + titlePaint.MeasureText("K/D", ref textBounds); + canvas.DrawText("K/D", 698, 261 - textBounds.Top, titlePaint); - titleFont.MeasureText("Top 25", out textBounds); - canvas.DrawText("Top 25", 837, 261 - textBounds.Top, titleFont, titlePaint); + titlePaint.MeasureText("Top 25", ref textBounds); + canvas.DrawText("Top 25", 837, 261 - textBounds.Top, titlePaint); // Duos var duosBoxRect = new SKRoundRect(SKRect.Create(996, 159, 459, 185), 30); DrawBlurredRoundRect(bitmap, duosBoxRect); canvas.DrawRoundRect(duosBoxRect, boxPaint); - boxTitleFont.MeasureText("DUOS", out textBounds); - canvas.DrawText("DUOS", 1006, 134 - textBounds.Top, boxTitleFont, boxTitlePaint); + boxTitlePaint.MeasureText("DUOS", ref textBounds); + canvas.DrawText("DUOS", 1006, 134 - textBounds.Top, boxTitlePaint); var duosIcon = await assets.GetBitmap("Assets/Images/Stats/PlaylistIcons/duos.png"); // don't dispose canvas.DrawBitmap(duosIcon, new SKPoint(1133, 134)); - titleFont.MeasureText("Games", out textBounds); - canvas.DrawText("Games", 1016, 184 - textBounds.Top, titleFont, titlePaint); + titlePaint.MeasureText("Games", ref textBounds); + canvas.DrawText("Games", 1016, 184 - textBounds.Top, titlePaint); - titleFont.MeasureText("Wins", out textBounds); - canvas.DrawText("Wins", 1177, 184 - textBounds.Top, titleFont, titlePaint); + titlePaint.MeasureText("Wins", ref textBounds); + canvas.DrawText("Wins", 1177, 184 - textBounds.Top, titlePaint); - titleFont.MeasureText("Win%", out textBounds); - canvas.DrawText("Win%", 1316, 184 - textBounds.Top, titleFont, titlePaint); + titlePaint.MeasureText("Win%", ref textBounds); + canvas.DrawText("Win%", 1316, 184 - textBounds.Top, titlePaint); - titleFont.MeasureText("Kills", out textBounds); - canvas.DrawText("Kills", 1016, 261 - textBounds.Top, titleFont, titlePaint); + titlePaint.MeasureText("Kills", ref textBounds); + canvas.DrawText("Kills", 1016, 261 - textBounds.Top, titlePaint); - titleFont.MeasureText("K/D", out textBounds); - canvas.DrawText("K/D", 1177, 261 - textBounds.Top, titleFont, titlePaint); + titlePaint.MeasureText("K/D", ref textBounds); + canvas.DrawText("K/D", 1177, 261 - textBounds.Top, titlePaint); - titleFont.MeasureText("Top 12", out textBounds); - canvas.DrawText("Top 12", 1316, 261 - textBounds.Top, titleFont, titlePaint); + titlePaint.MeasureText("Top 12", ref textBounds); + canvas.DrawText("Top 12", 1316, 261 - textBounds.Top, titlePaint); // Trios var triosBoxRect = new SKRoundRect(SKRect.Create(517, 389, 459, 185), 30); DrawBlurredRoundRect(bitmap, triosBoxRect); canvas.DrawRoundRect(triosBoxRect, boxPaint); - boxTitleFont.MeasureText("TRIOS", out textBounds); - canvas.DrawText("TRIOS", 527, 364 - textBounds.Top, boxTitleFont, boxTitlePaint); + boxTitlePaint.MeasureText("TRIOS", ref textBounds); + canvas.DrawText("TRIOS", 527, 364 - textBounds.Top, boxTitlePaint); var triosIcon = await assets.GetBitmap(@"Assets/Images/Stats/PlaylistIcons/trios.png"); // don't dispose canvas.DrawBitmap(triosIcon, new SKPoint(663, 364)); - titleFont.MeasureText("Games", out textBounds); - canvas.DrawText("Games", 537, 414 - textBounds.Top, titleFont, titlePaint); + titlePaint.MeasureText("Games", ref textBounds); + canvas.DrawText("Games", 537, 414 - textBounds.Top, titlePaint); - titleFont.MeasureText("Wins", out textBounds); - canvas.DrawText("Wins", 698, 414 - textBounds.Top, titleFont, titlePaint); + titlePaint.MeasureText("Wins", ref textBounds); + canvas.DrawText("Wins", 698, 414 - textBounds.Top, titlePaint); - titleFont.MeasureText("Win%", out textBounds); - canvas.DrawText("Win%", 837, 414 - textBounds.Top, titleFont, titlePaint); + titlePaint.MeasureText("Win%", ref textBounds); + canvas.DrawText("Win%", 837, 414 - textBounds.Top, titlePaint); - titleFont.MeasureText("Kills", out textBounds); - canvas.DrawText("Kills", 537, 491 - textBounds.Top, titleFont, titlePaint); + titlePaint.MeasureText("Kills", ref textBounds); + canvas.DrawText("Kills", 537, 491 - textBounds.Top, titlePaint); - titleFont.MeasureText("K/D", out textBounds); - canvas.DrawText("K/D", 698, 491 - textBounds.Top, titleFont, titlePaint); + titlePaint.MeasureText("K/D", ref textBounds); + canvas.DrawText("K/D", 698, 491 - textBounds.Top, titlePaint); - titleFont.MeasureText("Top 6", out textBounds); - canvas.DrawText("Top 6", 837, 491 - textBounds.Top, titleFont, titlePaint); + titlePaint.MeasureText("Top 6", ref textBounds); + canvas.DrawText("Top 6", 837, 491 - textBounds.Top, titlePaint); // Squads var squadsBoxRect = new SKRoundRect(SKRect.Create(996, 389, 459, 185), 30); DrawBlurredRoundRect(bitmap, squadsBoxRect); canvas.DrawRoundRect(squadsBoxRect, boxPaint); - boxTitleFont.MeasureText("SQUADS", out textBounds); - canvas.DrawText("SQUADS", 1006, 364 - textBounds.Top, boxTitleFont, boxTitlePaint); + boxTitlePaint.MeasureText("SQUADS", ref textBounds); + canvas.DrawText("SQUADS", 1006, 364 - textBounds.Top, boxTitlePaint); var squadsIcon = await assets.GetBitmap(@"Assets/Images/Stats/PlaylistIcons/squads.png"); // don't dispose canvas.DrawBitmap(squadsIcon, new SKPoint(1191, 364)); - titleFont.MeasureText("Games", out textBounds); - canvas.DrawText("Games", 1016, 414 - textBounds.Top, titleFont, titlePaint); + titlePaint.MeasureText("Games", ref textBounds); + canvas.DrawText("Games", 1016, 414 - textBounds.Top, titlePaint); - titleFont.MeasureText("Wins", out textBounds); - canvas.DrawText("Wins", 1177, 414 - textBounds.Top, titleFont, titlePaint); + titlePaint.MeasureText("Wins", ref textBounds); + canvas.DrawText("Wins", 1177, 414 - textBounds.Top, titlePaint); - titleFont.MeasureText("Win%", out textBounds); - canvas.DrawText("Win%", 1316, 414 - textBounds.Top, titleFont, titlePaint); + titlePaint.MeasureText("Win%", ref textBounds); + canvas.DrawText("Win%", 1316, 414 - textBounds.Top, titlePaint); - titleFont.MeasureText("Kills", out textBounds); - canvas.DrawText("Kills", 1016, 491 - textBounds.Top, titleFont, titlePaint); + titlePaint.MeasureText("Kills", ref textBounds); + canvas.DrawText("Kills", 1016, 491 - textBounds.Top, titlePaint); - titleFont.MeasureText("K/D", out textBounds); - canvas.DrawText("K/D", 1177, 491 - textBounds.Top, titleFont, titlePaint); + titlePaint.MeasureText("K/D", ref textBounds); + canvas.DrawText("K/D", 1177, 491 - textBounds.Top, titlePaint); - titleFont.MeasureText("Top 6", out textBounds); - canvas.DrawText("Top 6", 1316, 491 - textBounds.Top, titleFont, titlePaint); + titlePaint.MeasureText("Top 6", ref textBounds); + canvas.DrawText("Top 6", 1316, 491 - textBounds.Top, titlePaint); if (type == StatsType.Normal) { @@ -331,26 +335,26 @@ await assets.GetBitmap("data/images/{0}", DrawBlurredRoundRect(bitmap, teamsBoxRect); canvas.DrawRoundRect(teamsBoxRect, boxPaint); - boxTitleFont.MeasureText("TEAMS", out textBounds); - canvas.DrawText("TEAMS", 527, 594 - textBounds.Top, boxTitleFont, boxTitlePaint); + boxTitlePaint.MeasureText("TEAMS", ref textBounds); + canvas.DrawText("TEAMS", 527, 594 - textBounds.Top, boxTitlePaint); - var teamsIcon = await assets.GetBitmap(@"Assets/Images/Stats/PlaylistIcons/teams.png"); // don't dispose + var teamsIcon = await assets.GetBitmap("Assets/Images/Stats/PlaylistIcons/teams.png"); // don't dispose canvas.DrawBitmap(teamsIcon, new SKPoint(683, 594)); - titleFont.MeasureText("Games", out textBounds); - canvas.DrawText("Games", 537, 644 - textBounds.Top, titleFont, titlePaint); + titlePaint.MeasureText("Games", ref textBounds); + canvas.DrawText("Games", 537, 644 - textBounds.Top, titlePaint); - titleFont.MeasureText("Wins", out textBounds); - canvas.DrawText("Wins", 698, 644 - textBounds.Top, titleFont, titlePaint); + titlePaint.MeasureText("Wins", ref textBounds); + canvas.DrawText("Wins", 698, 644 - textBounds.Top, titlePaint); - titleFont.MeasureText("Win%", out textBounds); - canvas.DrawText("Win%", 837, 644 - textBounds.Top, titleFont, titlePaint); + titlePaint.MeasureText("Win%", ref textBounds); + canvas.DrawText("Win%", 837, 644 - textBounds.Top, titlePaint); - titleFont.MeasureText("Kills", out textBounds); - canvas.DrawText("Kills", 954, 644 - textBounds.Top, titleFont, titlePaint); + titlePaint.MeasureText("Kills", ref textBounds); + canvas.DrawText("Kills", 954, 644 - textBounds.Top, titlePaint); - titleFont.MeasureText("K/D", out textBounds); - canvas.DrawText("K/D", 1115, 644 - textBounds.Top, titleFont, titlePaint); + titlePaint.MeasureText("K/D", ref textBounds); + canvas.DrawText("K/D", 1115, 644 - textBounds.Top, titlePaint); } return bitmap; @@ -368,41 +372,55 @@ private async Task GenerateImage(Stats stats, StatsType type, SKBitmap var segoeFont = await assets.GetFont("Assets/Fonts/Segoe.ttf"); // don't dispose using var namePaint = new SKPaint(); - using var nameFont = new SKFont(segoeFont, 64); namePaint.IsAntialias = true; namePaint.Color = SKColors.White; + namePaint.Typeface = segoeFont; + namePaint.TextSize = 64; + namePaint.FilterQuality = SKFilterQuality.Medium; using var titlePaint = new SKPaint(); - using var titleFont = new SKFont(segoeFont, 20); titlePaint.IsAntialias = true; titlePaint.Color = SKColors.LightGray; + titlePaint.Typeface = segoeFont; + titlePaint.TextSize = 20; + titlePaint.FilterQuality = SKFilterQuality.Medium; using var valuePaint = new SKPaint(); - using var valueFont = new SKFont(fortniteFont, 35); valuePaint.IsAntialias = true; valuePaint.Color = SKColors.White; + valuePaint.Typeface = fortniteFont; + valuePaint.TextSize = 35; + valuePaint.FilterQuality = SKFilterQuality.Medium; using var divisionPaint = new SKPaint(); - using var divisionFont = new SKFont(fortniteFont, 35); divisionPaint.IsAntialias = true; divisionPaint.Color = SKColors.White; + divisionPaint.Typeface = fortniteFont; + divisionPaint.TextSize = 35; + divisionPaint.FilterQuality = SKFilterQuality.Medium; using var rankProgressPaint = new SKPaint(); - using var rankProgressFont = new SKFont(segoeFont, 16); rankProgressPaint.IsAntialias = true; - rankProgressPaint.Color = SKColors.White.WithAlpha((int) (255 * 0.7)); + rankProgressPaint.Color = SKColors.White.WithAlpha((int)(255 * 0.7)); + rankProgressPaint.Typeface = segoeFont; + rankProgressPaint.TextSize = 16; + rankProgressPaint.FilterQuality = SKFilterQuality.Medium; using var rankingPaint = new SKPaint(); - using var rankingFont = new SKFont(fortniteFont, 20); rankingPaint.IsAntialias = true; rankingPaint.Color = SKColors.White; + rankingPaint.Typeface = fortniteFont; + rankingPaint.TextSize = 20; + rankingPaint.FilterQuality = SKFilterQuality.Medium; + + var textBounds = new SKRect(); var inputIcon = await assets.GetBitmap($"Assets/Images/Stats/InputTypes/{stats.InputType}.png"); // don't dispose canvas.DrawBitmap(inputIcon, 50, 50); - nameFont.MeasureText(stats.PlayerName, out var textBounds); - canvas.DrawText(stats.PlayerName, 159, 58 - textBounds.Top, nameFont, namePaint); + namePaint.MeasureText(stats.PlayerName, ref textBounds); + canvas.DrawText(stats.PlayerName, 159, 58 - textBounds.Top, namePaint); if (stats.IsVerified) { @@ -431,21 +449,23 @@ await assets.GetBitmap( $"Assets/Images/Stats/DivisionIcons/{divisionAssetName}.png"); // don't dispose canvas.DrawBitmap(divisionIconBitmap, x - divisionIconBitmap!.Width / 2f, 109); - divisionFont.MeasureText(rankedStatsEntry.CurrentDivisionName, out textBounds); - canvas.DrawText(rankedStatsEntry.CurrentDivisionName, x - (int) (textBounds.Width / 2), 206 - textBounds.Top, divisionFont, divisionPaint); + divisionPaint.MeasureText(rankedStatsEntry.CurrentDivisionName, ref textBounds); + canvas.DrawText(rankedStatsEntry.CurrentDivisionName, x - (int)(textBounds.Width / 2), + 206 - textBounds.Top, divisionPaint); + if (rankedStatsEntry.Ranking == null) { const int maxBarWidth = 130, barHeight = 6; - var progressText = $"{(int) (rankedStatsEntry.Progress * 100)}%"; - rankProgressFont.MeasureText(progressText, out textBounds); + var progressText = $"{(int)(rankedStatsEntry.Progress * 100)}%"; + rankProgressPaint.MeasureText(progressText, ref textBounds); var barX = x - textBounds.Width / 2f - maxBarWidth / 2f; using var barBackgroundPaint = new SKPaint(); barBackgroundPaint.IsAntialias = true; - barBackgroundPaint.Color = SKColors.White.WithAlpha((int) (.2 * 255)); + barBackgroundPaint.Color = SKColors.White.WithAlpha((int)(.2 * 255)); canvas.DrawRoundRect(barX, 250, maxBarWidth, barHeight, 10, 10, barBackgroundPaint); - var rankProgressBarWidth = (int) (maxBarWidth * rankedStatsEntry.Progress); + var rankProgressBarWidth = (int)(maxBarWidth * rankedStatsEntry.Progress); if (rankProgressBarWidth > 0) { rankProgressBarWidth = Math.Max(rankProgressBarWidth, barHeight); @@ -463,69 +483,70 @@ await assets.GetBitmap( canvas.DrawRoundRect(barX, 250, rankProgressBarWidth, barHeight, 10, 10, battlePassBarPaint); } - canvas.DrawText(progressText, barX + maxBarWidth + 7, 247 - textBounds.Top, rankProgressFont, rankProgressPaint); + canvas.DrawText(progressText, barX + maxBarWidth + 7, 247 - textBounds.Top, rankProgressPaint); } else { - rankingFont.MeasureText(rankedStatsEntry.Ranking, out textBounds); - canvas.DrawText(rankedStatsEntry.Ranking, x - (int) (textBounds.Width / 2), 245 - textBounds.Top, rankingFont, rankingPaint); + rankingPaint.MeasureText(rankedStatsEntry.Ranking, ref textBounds); + canvas.DrawText(rankedStatsEntry.Ranking, x - (int)(textBounds.Width / 2), 245 - textBounds.Top, + rankingPaint); } } - valueFont.MeasureText(stats.Competitive.Earnings, out textBounds); - canvas.DrawText(stats.Competitive.Earnings, 70, 365 - textBounds.Top, valueFont, valuePaint); + valuePaint.MeasureText(stats.Competitive.Earnings, ref textBounds); + canvas.DrawText(stats.Competitive.Earnings, 70, 365 - textBounds.Top, valuePaint); - valueFont.MeasureText(stats.Competitive.PowerRanking, out textBounds); - canvas.DrawText(stats.Competitive.PowerRanking, 250, 365 - textBounds.Top, valueFont, valuePaint); + valuePaint.MeasureText(stats.Competitive.PowerRanking, ref textBounds); + canvas.DrawText(stats.Competitive.PowerRanking, 250, 365 - textBounds.Top, valuePaint); - valueFont.MeasureText(stats.Overall.MatchesPlayed, out textBounds); - canvas.DrawText(stats.Overall.MatchesPlayed, 70, 441 - textBounds.Top, valueFont, valuePaint); + valuePaint.MeasureText(stats.Overall.MatchesPlayed, ref textBounds); + canvas.DrawText(stats.Overall.MatchesPlayed, 70, 441 - textBounds.Top, valuePaint); - valueFont.MeasureText(stats.Overall.Wins, out textBounds); - canvas.DrawText(stats.Overall.Wins, 231, 441 - textBounds.Top, valueFont, valuePaint); + valuePaint.MeasureText(stats.Overall.Wins, ref textBounds); + canvas.DrawText(stats.Overall.Wins, 231, 441 - textBounds.Top, valuePaint); - valueFont.MeasureText(stats.Overall.WinRatio, out textBounds); - canvas.DrawText(stats.Overall.WinRatio, 370, 441 - textBounds.Top, valueFont, valuePaint); + valuePaint.MeasureText(stats.Overall.WinRatio, ref textBounds); + canvas.DrawText(stats.Overall.WinRatio, 370, 441 - textBounds.Top, valuePaint); - valueFont.MeasureText(stats.Overall.Kills, out textBounds); - canvas.DrawText(stats.Overall.Kills, 70, 518 - textBounds.Top, valueFont, valuePaint); + valuePaint.MeasureText(stats.Overall.Kills, ref textBounds); + canvas.DrawText(stats.Overall.Kills, 70, 518 - textBounds.Top, valuePaint); - valueFont.MeasureText(stats.Overall.KD, out textBounds); - canvas.DrawText(stats.Overall.KD, 231, 518 - textBounds.Top, valueFont, valuePaint); + valuePaint.MeasureText(stats.Overall.KD, ref textBounds); + canvas.DrawText(stats.Overall.KD, 231, 518 - textBounds.Top, valuePaint); } else { - valueFont.MeasureText(stats.Overall.MatchesPlayed, out textBounds); - canvas.DrawText(stats.Overall.MatchesPlayed, 70, 211 - textBounds.Top, valueFont, valuePaint); + valuePaint.MeasureText(stats.Overall.MatchesPlayed, ref textBounds); + canvas.DrawText(stats.Overall.MatchesPlayed, 70, 211 - textBounds.Top, valuePaint); - valueFont.MeasureText(stats.Overall.Wins, out textBounds); - canvas.DrawText(stats.Overall.Wins, 231, 211 - textBounds.Top, valueFont, valuePaint); + valuePaint.MeasureText(stats.Overall.Wins, ref textBounds); + canvas.DrawText(stats.Overall.Wins, 231, 211 - textBounds.Top, valuePaint); - valueFont.MeasureText(stats.Overall.WinRatio, out textBounds); - canvas.DrawText(stats.Overall.WinRatio, 370, 211 - textBounds.Top, valueFont, valuePaint); + valuePaint.MeasureText(stats.Overall.WinRatio, ref textBounds); + canvas.DrawText(stats.Overall.WinRatio, 370, 211 - textBounds.Top, valuePaint); - valueFont.MeasureText(stats.Overall.Kills, out textBounds); - canvas.DrawText(stats.Overall.Kills, 70, 288 - textBounds.Top, valueFont, valuePaint); + valuePaint.MeasureText(stats.Overall.Kills, ref textBounds); + canvas.DrawText(stats.Overall.Kills, 70, 288 - textBounds.Top, valuePaint); - valueFont.MeasureText(stats.Overall.KD, out textBounds); - canvas.DrawText(stats.Overall.KD, 231, 288 - textBounds.Top, valueFont, valuePaint); + valuePaint.MeasureText(stats.Overall.KD, ref textBounds); + canvas.DrawText(stats.Overall.KD, 231, 288 - textBounds.Top, valuePaint); - valueFont.MeasureText(stats.Playtime.Days, out textBounds); - canvas.DrawText(stats.Playtime.Days, 70, 369 - textBounds.Top, valueFont, valuePaint); + valuePaint.MeasureText(stats.Playtime.Days, ref textBounds); + canvas.DrawText(stats.Playtime.Days, 70, 369 - textBounds.Top, valuePaint); - valueFont.MeasureText(stats.Playtime.Hours, out textBounds); - canvas.DrawText(stats.Playtime.Hours, 147, 369 - textBounds.Top, valueFont, valuePaint); + valuePaint.MeasureText(stats.Playtime.Hours, ref textBounds); + canvas.DrawText(stats.Playtime.Hours, 147, 369 - textBounds.Top, valuePaint); - valueFont.MeasureText(stats.Playtime.Minutes, out textBounds); - canvas.DrawText(stats.Playtime.Minutes, 213, 369 - textBounds.Top, valueFont, valuePaint); + valuePaint.MeasureText(stats.Playtime.Minutes, ref textBounds); + canvas.DrawText(stats.Playtime.Minutes, 213, 369 - textBounds.Top, valuePaint); - var battlePassLevel = ((int) stats.BattlePassLevel).ToString(); - valueFont.MeasureText(battlePassLevel, out textBounds); - canvas.DrawText(battlePassLevel, 70, 479 - textBounds.Top, valueFont, valuePaint); + var battlePassLevel = ((int)stats.BattlePassLevel).ToString(); + valuePaint.MeasureText(battlePassLevel, ref textBounds); + canvas.DrawText(battlePassLevel, 70, 479 - textBounds.Top, valuePaint); const int maxBarWidth = 309, barHeight = 20; - var battlePassBarWidth = (int) (maxBarWidth * (stats.BattlePassLevel - (int) stats.BattlePassLevel)); + var battlePassBarWidth = (int)(maxBarWidth * (stats.BattlePassLevel - (int)stats.BattlePassLevel)); if (battlePassBarWidth > 0) { battlePassBarWidth = Math.Max(battlePassBarWidth, barHeight); @@ -545,97 +566,97 @@ await assets.GetBitmap( } } - valueFont.MeasureText(stats.Solo.MatchesPlayed, out textBounds); - canvas.DrawText(stats.Solo.MatchesPlayed, 537, 211 - textBounds.Top, valueFont, valuePaint); + valuePaint.MeasureText(stats.Solo.MatchesPlayed, ref textBounds); + canvas.DrawText(stats.Solo.MatchesPlayed, 537, 211 - textBounds.Top, valuePaint); - valueFont.MeasureText(stats.Solo.Wins, out textBounds); - canvas.DrawText(stats.Solo.Wins, 698, 211 - textBounds.Top, valueFont, valuePaint); + valuePaint.MeasureText(stats.Solo.Wins, ref textBounds); + canvas.DrawText(stats.Solo.Wins, 698, 211 - textBounds.Top, valuePaint); - valueFont.MeasureText(stats.Solo.WinRatio, out textBounds); - canvas.DrawText(stats.Solo.WinRatio, 837, 211 - textBounds.Top, valueFont, valuePaint); + valuePaint.MeasureText(stats.Solo.WinRatio, ref textBounds); + canvas.DrawText(stats.Solo.WinRatio, 837, 211 - textBounds.Top, valuePaint); - valueFont.MeasureText(stats.Solo.Kills, out textBounds); - canvas.DrawText(stats.Solo.Kills, 537, 288 - textBounds.Top, valueFont, valuePaint); + valuePaint.MeasureText(stats.Solo.Kills, ref textBounds); + canvas.DrawText(stats.Solo.Kills, 537, 288 - textBounds.Top, valuePaint); - valueFont.MeasureText(stats.Solo.KD, out textBounds); - canvas.DrawText(stats.Solo.KD, 698, 288 - textBounds.Top, valueFont, valuePaint); + valuePaint.MeasureText(stats.Solo.KD, ref textBounds); + canvas.DrawText(stats.Solo.KD, 698, 288 - textBounds.Top, valuePaint); - valueFont.MeasureText(stats.Solo.Top25, out textBounds); - canvas.DrawText(stats.Solo.Top25, 837, 288 - textBounds.Top, valueFont, valuePaint); + valuePaint.MeasureText(stats.Solo.Top25, ref textBounds); + canvas.DrawText(stats.Solo.Top25, 837, 288 - textBounds.Top, valuePaint); - valueFont.MeasureText(stats.Duos.MatchesPlayed, out textBounds); - canvas.DrawText(stats.Duos.MatchesPlayed, 1016, 211 - textBounds.Top, valueFont, valuePaint); + valuePaint.MeasureText(stats.Duos.MatchesPlayed, ref textBounds); + canvas.DrawText(stats.Duos.MatchesPlayed, 1016, 211 - textBounds.Top, valuePaint); - valueFont.MeasureText(stats.Duos.Wins, out textBounds); - canvas.DrawText(stats.Duos.Wins, 1177, 211 - textBounds.Top, valueFont, valuePaint); + valuePaint.MeasureText(stats.Duos.Wins, ref textBounds); + canvas.DrawText(stats.Duos.Wins, 1177, 211 - textBounds.Top, valuePaint); - valueFont.MeasureText(stats.Duos.WinRatio, out textBounds); - canvas.DrawText(stats.Duos.WinRatio, 1316, 211 - textBounds.Top, valueFont, valuePaint); + valuePaint.MeasureText(stats.Duos.WinRatio, ref textBounds); + canvas.DrawText(stats.Duos.WinRatio, 1316, 211 - textBounds.Top, valuePaint); - valueFont.MeasureText(stats.Duos.Kills, out textBounds); - canvas.DrawText(stats.Duos.Kills, 1016, 288 - textBounds.Top, valueFont, valuePaint); + valuePaint.MeasureText(stats.Duos.Kills, ref textBounds); + canvas.DrawText(stats.Duos.Kills, 1016, 288 - textBounds.Top, valuePaint); - valueFont.MeasureText(stats.Duos.KD, out textBounds); - canvas.DrawText(stats.Duos.KD, 1177, 288 - textBounds.Top, valueFont, valuePaint); + valuePaint.MeasureText(stats.Duos.KD, ref textBounds); + canvas.DrawText(stats.Duos.KD, 1177, 288 - textBounds.Top, valuePaint); - valueFont.MeasureText(stats.Duos.Top12, out textBounds); - canvas.DrawText(stats.Duos.Top12, 1316, 288 - textBounds.Top, valueFont, valuePaint); + valuePaint.MeasureText(stats.Duos.Top12, ref textBounds); + canvas.DrawText(stats.Duos.Top12, 1316, 288 - textBounds.Top, valuePaint); - valueFont.MeasureText(stats.Trios.MatchesPlayed, out textBounds); - canvas.DrawText(stats.Trios.MatchesPlayed, 537, 441 - textBounds.Top, valueFont, valuePaint); + valuePaint.MeasureText(stats.Trios.MatchesPlayed, ref textBounds); + canvas.DrawText(stats.Trios.MatchesPlayed, 537, 441 - textBounds.Top, valuePaint); - valueFont.MeasureText(stats.Trios.Wins, out textBounds); - canvas.DrawText(stats.Trios.Wins, 698, 441 - textBounds.Top, valueFont, valuePaint); + valuePaint.MeasureText(stats.Trios.Wins, ref textBounds); + canvas.DrawText(stats.Trios.Wins, 698, 441 - textBounds.Top, valuePaint); - valueFont.MeasureText(stats.Trios.WinRatio, out textBounds); - canvas.DrawText(stats.Trios.WinRatio, 837, 441 - textBounds.Top, valueFont, valuePaint); + valuePaint.MeasureText(stats.Trios.WinRatio, ref textBounds); + canvas.DrawText(stats.Trios.WinRatio, 837, 441 - textBounds.Top, valuePaint); - valueFont.MeasureText(stats.Trios.Kills, out textBounds); - canvas.DrawText(stats.Trios.Kills, 537, 518 - textBounds.Top, valueFont, valuePaint); + valuePaint.MeasureText(stats.Trios.Kills, ref textBounds); + canvas.DrawText(stats.Trios.Kills, 537, 518 - textBounds.Top, valuePaint); - valueFont.MeasureText(stats.Trios.KD, out textBounds); - canvas.DrawText(stats.Trios.KD, 698, 518 - textBounds.Top, valueFont, valuePaint); + valuePaint.MeasureText(stats.Trios.KD, ref textBounds); + canvas.DrawText(stats.Trios.KD, 698, 518 - textBounds.Top, valuePaint); - valueFont.MeasureText(stats.Trios.Top6, out textBounds); - canvas.DrawText(stats.Trios.Top6, 837, 518 - textBounds.Top, valueFont, valuePaint); + valuePaint.MeasureText(stats.Trios.Top6, ref textBounds); + canvas.DrawText(stats.Trios.Top6, 837, 518 - textBounds.Top, valuePaint); - valueFont.MeasureText(stats.Squads.MatchesPlayed, out textBounds); - canvas.DrawText(stats.Squads.MatchesPlayed, 1016, 441 - textBounds.Top, valueFont, valuePaint); + valuePaint.MeasureText(stats.Squads.MatchesPlayed, ref textBounds); + canvas.DrawText(stats.Squads.MatchesPlayed, 1016, 441 - textBounds.Top, valuePaint); - valueFont.MeasureText(stats.Squads.Wins, out textBounds); - canvas.DrawText(stats.Squads.Wins, 1177, 441 - textBounds.Top, valueFont, valuePaint); + valuePaint.MeasureText(stats.Squads.Wins, ref textBounds); + canvas.DrawText(stats.Squads.Wins, 1177, 441 - textBounds.Top, valuePaint); - valueFont.MeasureText(stats.Squads.WinRatio, out textBounds); - canvas.DrawText(stats.Squads.WinRatio, 1316, 441 - textBounds.Top, valueFont, valuePaint); + valuePaint.MeasureText(stats.Squads.WinRatio, ref textBounds); + canvas.DrawText(stats.Squads.WinRatio, 1316, 441 - textBounds.Top, valuePaint); - valueFont.MeasureText(stats.Squads.Kills, out textBounds); - canvas.DrawText(stats.Squads.Kills, 1016, 518 - textBounds.Top, valueFont, valuePaint); + valuePaint.MeasureText(stats.Squads.Kills, ref textBounds); + canvas.DrawText(stats.Squads.Kills, 1016, 518 - textBounds.Top, valuePaint); - valueFont.MeasureText(stats.Squads.KD, out textBounds); - canvas.DrawText(stats.Squads.KD, 1177, 518 - textBounds.Top, valueFont, valuePaint); + valuePaint.MeasureText(stats.Squads.KD, ref textBounds); + canvas.DrawText(stats.Squads.KD, 1177, 518 - textBounds.Top, valuePaint); - valueFont.MeasureText(stats.Squads.Top6, out textBounds); - canvas.DrawText(stats.Squads.Top6, 1316, 518 - textBounds.Top, valueFont, valuePaint); + valuePaint.MeasureText(stats.Squads.Top6, ref textBounds); + canvas.DrawText(stats.Squads.Top6, 1316, 518 - textBounds.Top, valuePaint); if (type == StatsType.Normal && stats.Teams != null) { - valueFont.MeasureText(stats.Teams.MatchesPlayed, out textBounds); - canvas.DrawText(stats.Teams.MatchesPlayed, 537, 671 - textBounds.Top, valueFont, valuePaint); + valuePaint.MeasureText(stats.Teams.MatchesPlayed, ref textBounds); + canvas.DrawText(stats.Teams.MatchesPlayed, 537, 671 - textBounds.Top, valuePaint); - valueFont.MeasureText(stats.Teams.Wins, out textBounds); - canvas.DrawText(stats.Teams.Wins, 698, 671 - textBounds.Top, valueFont, valuePaint); + valuePaint.MeasureText(stats.Teams.Wins, ref textBounds); + canvas.DrawText(stats.Teams.Wins, 698, 671 - textBounds.Top, valuePaint); - valueFont.MeasureText(stats.Teams.WinRatio, out textBounds); - canvas.DrawText(stats.Teams.WinRatio, 837, 671 - textBounds.Top, valueFont, valuePaint); + valuePaint.MeasureText(stats.Teams.WinRatio, ref textBounds); + canvas.DrawText(stats.Teams.WinRatio, 837, 671 - textBounds.Top, valuePaint); - valueFont.MeasureText(stats.Teams.Kills, out textBounds); - canvas.DrawText(stats.Teams.Kills, 954, 671 - textBounds.Top, valueFont, valuePaint); + valuePaint.MeasureText(stats.Teams.Kills, ref textBounds); + canvas.DrawText(stats.Teams.Kills, 954, 671 - textBounds.Top, valuePaint); - valueFont.MeasureText(stats.Teams.KD, out textBounds); - canvas.DrawText(stats.Teams.KD, 1115, 671 - textBounds.Top, valueFont, valuePaint); + valuePaint.MeasureText(stats.Teams.KD, ref textBounds); + canvas.DrawText(stats.Teams.KD, 1115, 671 - textBounds.Top, valuePaint); } return bitmap; diff --git a/Controllers/UtilsImageController.cs b/Controllers/UtilsImageController.cs index b80d931..e7bfd5b 100644 --- a/Controllers/UtilsImageController.cs +++ b/Controllers/UtilsImageController.cs @@ -8,7 +8,7 @@ namespace EasyFortniteStats_ImageApi.Controllers; [ApiController] [Route("utils")] -public class UtilsImageController(SharedAssets assets) : ControllerBase +public class UtilsImageController(SharedAssets assets, ILogger logger) : ControllerBase { [HttpGet("collectGarbage")] public IActionResult CollectGarbage() @@ -20,17 +20,17 @@ public IActionResult CollectGarbage() [HttpPost("progressBar")] public async Task GenerateProgressBar(ProgressBar progressBar) { - Console.WriteLine("Progress Bar request"); + logger.LogInformation("Progress Bar request received"); using var bitmap = new SKBitmap(568, 30); using var canvas = new SKCanvas(bitmap); using var barBackgroundPaint = new SKPaint(); barBackgroundPaint.IsAntialias = true; - barBackgroundPaint.Color = SKColors.White.WithAlpha((int) (.3 * 255)); + barBackgroundPaint.Color = SKColors.White.WithAlpha((int)(.3 * 255)); canvas.DrawRoundRect(0, bitmap.Height / 2f - 20 / 2f, 500, 20, 10, 10, barBackgroundPaint); - var barWidth = (int) (500 * progressBar.Progress); + var barWidth = (int)(500 * progressBar.Progress); if (barWidth > 0) { barWidth = barWidth < 20 ? 20 : barWidth; @@ -47,25 +47,28 @@ public async Task GenerateProgressBar(ProgressBar progressBar) } var segoeFont = await assets.GetFont("Assets/Fonts/Segoe.ttf"); + var textBounds = new SKRect(); using var textPaint = new SKPaint(); - using var textFont = new SKFont(segoeFont, 20); textPaint.IsAntialias = true; textPaint.Color = SKColors.White; + textPaint.TextSize = 20; + textPaint.Typeface = segoeFont; - textFont.MeasureText(progressBar.Text, out var textBounds); - canvas.DrawText(progressBar.Text, 500 + 5, (float) bitmap.Height / 2 - textBounds.MidY, textFont, textPaint); + textPaint.MeasureText(progressBar.Text, ref textBounds); + canvas.DrawText(progressBar.Text, 500 + 5, (float)bitmap.Height / 2 - textBounds.MidY, textPaint); if (progressBar.BarText != null) { using var barTextPaint = new SKPaint(); - using var barTextFont = new SKFont(segoeFont, 15); barTextPaint.IsAntialias = true; barTextPaint.Color = SKColors.White; + barTextPaint.TextSize = 15; + barTextPaint.Typeface = segoeFont; - barTextFont.MeasureText(progressBar.BarText, out textBounds); + barTextPaint.MeasureText(progressBar.BarText, ref textBounds); canvas.DrawText(progressBar.BarText, (500 - textBounds.Width) / 2, - bitmap.Height / 2f - textBounds.MidY, barTextFont, barTextPaint); + bitmap.Height / 2f - textBounds.MidY, barTextPaint); } var data = bitmap.Encode(SKEncodedImageFormat.Png, 100); @@ -75,7 +78,7 @@ public async Task GenerateProgressBar(ProgressBar progressBar) [HttpPost("drop")] public async Task GenerateDropImage(Drop drop) { - Console.WriteLine("Drop Image request"); + logger.LogInformation("Drop Image request received"); var mapBitmap = await assets.GetBitmap( $"data/images/map/{drop.Locale}.png"); // don't dispose TODO: Clear caching on bg change @@ -100,7 +103,7 @@ await assets.GetBitmap( var mx = (drop.X + worldRadius) / (worldRadius * 2f) * bitmap.Width + xOffset; var my = (drop.Y + worldRadius) / (worldRadius * 2f) * bitmap.Height + yOffset; - canvas.DrawBitmap(markerBitmap, mx - (float) markerBitmap!.Width / 2, my - markerBitmap.Height); + canvas.DrawBitmap(markerBitmap, mx - (float)markerBitmap!.Width / 2, my - markerBitmap.Height); var data = bitmap.Encode(SKEncodedImageFormat.Jpeg, 100); return File(data.AsStream(true), "image/jpeg"); diff --git a/EasyFortniteStats-ImageApi.csproj b/EasyFortniteStats-ImageApi.csproj index a019066..d6316f4 100644 --- a/EasyFortniteStats-ImageApi.csproj +++ b/EasyFortniteStats-ImageApi.csproj @@ -11,8 +11,8 @@ - - + + diff --git a/ImageUtils.cs b/ImageUtils.cs index 019db4c..3ca31ff 100644 --- a/ImageUtils.cs +++ b/ImageUtils.cs @@ -5,18 +5,21 @@ namespace EasyFortniteStats_ImageApi; public class ImageUtils { + private static readonly ILogger Logger = + LoggerFactory.Create(builder => builder.AddConsole()).CreateLogger(); + public static void BitmapPostEvictionCallback(object key, object? value, EvictionReason reason, object? state) { - Console.WriteLine($"MemoryCache: Disposing {key} | Reason: {reason}"); + Logger.LogDebug("MemoryCache: Disposing {Key} | Reason: {Reason}", key, reason); if (value is null) return; - var bmp = (SKBitmap) value; + var bmp = (SKBitmap)value; bmp.Dispose(); } - public static async Task GenerateDiscordBox(SharedAssets _assets, string username, + public static async Task GenerateDiscordBox(SharedAssets assets, string username, float resizeFactor = 1.0f) { - var segoeFont = await _assets.GetFont("Assets/Fonts/Segoe.ttf"); // don't dispose + var segoeFont = await assets.GetFont("Assets/Fonts/Segoe.ttf"); // don't dispose using var discordTagTextPaint = new SKPaint(); discordTagTextPaint.IsAntialias = true; @@ -28,8 +31,8 @@ public static async Task GenerateDiscordBox(SharedAssets _assets, stri discordTagTextPaint.MeasureText(username, ref discordTagTextBounds); var imageInfo = new SKImageInfo( - (int) Math.Min(discordTagTextBounds.Width + (10 + 2 * 15 + 50) * resizeFactor, 459 * resizeFactor), - (int) (62 * resizeFactor)); + (int)Math.Min(discordTagTextBounds.Width + (10 + 2 * 15 + 50) * resizeFactor, 459 * resizeFactor), + (int)(62 * resizeFactor)); var bitmap = new SKBitmap(imageInfo); using var canvas = new SKCanvas(bitmap); @@ -39,14 +42,14 @@ public static async Task GenerateDiscordBox(SharedAssets _assets, stri discordBoxPaint.Color = new SKColor(88, 101, 242); canvas.DrawRoundRect(0, 0, imageInfo.Width, imageInfo.Height, discordBoxR, discordBoxR, discordBoxPaint); - var logoResizeWidth = (int) (50 * resizeFactor); - var discordLogoBitmap = await _assets.GetBitmap("Assets/Images/DiscordLogo.png"); // don't dispose + var logoResizeWidth = (int)(50 * resizeFactor); + var discordLogoBitmap = await assets.GetBitmap("Assets/Images/DiscordLogo.png"); // don't dispose // get height with the same aspect ratio - var logoResizeHeight = (int) (discordLogoBitmap!.Height * (logoResizeWidth / (float) discordLogoBitmap.Width)); + var logoResizeHeight = (int)(discordLogoBitmap!.Height * (logoResizeWidth / (float)discordLogoBitmap.Width)); var discordLogoBitmapResized = discordLogoBitmap.Resize(new SKImageInfo(logoResizeWidth, logoResizeHeight), SKFilterQuality.High); canvas.DrawBitmap(discordLogoBitmapResized, 10 * resizeFactor, - (float) (imageInfo.Height - discordLogoBitmapResized.Height) / 2); + (float)(imageInfo.Height - discordLogoBitmapResized.Height) / 2); while (discordTagTextBounds.Width + (10 + 2 * 15 + 50) * resizeFactor > imageInfo.Width) { @@ -55,19 +58,19 @@ public static async Task GenerateDiscordBox(SharedAssets _assets, stri } canvas.DrawText(username, (10 + 15) * resizeFactor + discordLogoBitmapResized.Width, - (float) imageInfo.Height / 2 - discordTagTextBounds.MidY, discordTagTextPaint); + (float)imageInfo.Height / 2 - discordTagTextBounds.MidY, discordTagTextPaint); return bitmap; } - public static SKBitmap RotateBitmap(SKBitmap bitmap, float angle) + private static SKBitmap RotateBitmap(SKBitmap bitmap, float angle) { var radians = MathF.PI * angle / 180; var sine = MathF.Abs(MathF.Sin(radians)); var cosine = MathF.Abs(MathF.Cos(radians)); int originalWidth = bitmap.Width, originalHeight = bitmap.Height; - var rotatedWidth = (int) (cosine * originalWidth + sine * originalHeight); - var rotatedHeight = (int) (cosine * originalHeight + sine * originalWidth); + var rotatedWidth = (int)(cosine * originalWidth + sine * originalHeight); + var rotatedHeight = (int)(cosine * originalHeight + sine * originalWidth); var rotatedBitmap = new SKBitmap(rotatedWidth, rotatedHeight); using var rotatedCanvas = new SKCanvas(rotatedBitmap); @@ -87,20 +90,18 @@ public static SKBitmap GenerateRarityStripe(int width, SKColor rarityColor) using var canvas = new SKCanvas(bitmap); using var paint = new SKPaint(); - { - paint.IsAntialias = true; - paint.Color = rarityColor; - paint.Style = SKPaintStyle.Fill; - - using var path = new SKPath(); - path.MoveTo(0, imageInfo.Height - 5); - path.LineTo(imageInfo.Width, 0); - path.LineTo(imageInfo.Width, imageInfo.Height - 6); - path.LineTo(0, imageInfo.Height); - path.Close(); - - canvas.DrawPath(path, paint); - } + paint.IsAntialias = true; + paint.Color = rarityColor; + paint.Style = SKPaintStyle.Fill; + + using var path = new SKPath(); + path.MoveTo(0, imageInfo.Height - 5); + path.LineTo(imageInfo.Width, 0); + path.LineTo(imageInfo.Width, imageInfo.Height - 6); + path.LineTo(0, imageInfo.Height); + path.Close(); + + canvas.DrawPath(path, paint); return bitmap; } diff --git a/Models/Shop.cs b/Models/Shop.cs index 766e009..29017f7 100644 --- a/Models/Shop.cs +++ b/Models/Shop.cs @@ -10,7 +10,7 @@ public class Shop public string? CreatorCodeTitle { get; set; } public string? CreatorCode { get; set; } public string? BackgroundImagePath { get; set; } - public ShopSection[] Sections { get; set; } + public ShopSection[] Sections { get; set; } } public class ShopSection @@ -52,11 +52,11 @@ public ShopSectionLocationData(string id, ShopLocationDataEntry? name, ShopEntry Name = name; Entries = entries; } - + public string Id { get; } public ShopLocationDataEntry? Name { get; } public ShopEntryLocationData[] Entries { get; } -} +} public class ShopEntryLocationData { @@ -67,7 +67,7 @@ public ShopEntryLocationData(string id, ShopLocationDataEntry name, ShopLocation Price = price; Banner = banner; } - + public string Id { get; } public ShopLocationDataEntry Name { get; } public ShopLocationDataEntry Price { get; } @@ -81,14 +81,14 @@ public ShopLocationDataEntry(int x, int y) X = x; Y = y; } - + public ShopLocationDataEntry(int x, int y, int maxWidth) { X = x; Y = y; MaxWidth = maxWidth; } - + public int X { get; } public int Y { get; } public int? MaxWidth { get; } diff --git a/Models/Stats.cs b/Models/Stats.cs index e78bac6..db85022 100644 --- a/Models/Stats.cs +++ b/Models/Stats.cs @@ -42,7 +42,7 @@ public class RankedStatsEntry public string HighestDivisionName { get; set; } public float Progress { get; set; } public string? Ranking { get; set; } - + public bool isUnranked() { return CurrentDivision == 0 && HighestDivision == 0 && Progress == 0.0; diff --git a/SharedAssets.cs b/SharedAssets.cs index c3655c7..8f827d3 100644 --- a/SharedAssets.cs +++ b/SharedAssets.cs @@ -4,16 +4,10 @@ namespace EasyFortniteStats_ImageApi; -public class SharedAssets +public class SharedAssets(IMemoryCache memoryCache) { - private static readonly MemoryCacheEntryOptions CacheOptions = new() {Priority = CacheItemPriority.NeverRemove}; + private static readonly MemoryCacheEntryOptions CacheOptions = new() { Priority = CacheItemPriority.NeverRemove }; private static readonly SemaphoreSlim Semaphore = new(1); - private readonly IMemoryCache _memoryCache; - - public SharedAssets(IMemoryCache memoryCache) - { - _memoryCache = memoryCache; - } public async ValueTask GetBitmap(string format, string? arg1) { @@ -27,12 +21,12 @@ public SharedAssets(IMemoryCache memoryCache) if (path is null) return null; var key = $"bmp_{path}"; - var cached = _memoryCache.Get(key); + var cached = memoryCache.Get(key); if (cached is not null) return cached; await Semaphore.WaitAsync(); - cached = _memoryCache.Get(key); + cached = memoryCache.Get(key); if (cached is not null) { Semaphore.Release(); @@ -41,14 +35,14 @@ public SharedAssets(IMemoryCache memoryCache) if (!File.Exists(path)) { - _memoryCache.Set(key, (SKBitmap?)null, CacheOptions); + memoryCache.Set(key, (SKBitmap?)null, CacheOptions); Semaphore.Release(); return null; } using var data = await ReadToSkData(path); // TODO: test if should dispose var bitmap = SKBitmap.Decode(data); - _memoryCache.Set(key, bitmap, CacheOptions); + memoryCache.Set(key, bitmap, CacheOptions); Semaphore.Release(); return bitmap; } @@ -56,12 +50,12 @@ public SharedAssets(IMemoryCache memoryCache) public async ValueTask GetFont(string path) { var key = $"font_{path}"; - var cached = _memoryCache.Get(key); + var cached = memoryCache.Get(key); if (cached is not null) return cached; await Semaphore.WaitAsync(); - cached = _memoryCache.Get(key); + cached = memoryCache.Get(key); if (cached is not null) { Semaphore.Release(); @@ -70,7 +64,7 @@ public async ValueTask GetFont(string path) using var data = await ReadToSkData(path); // TODO: test if should dispose var typeface = SKTypeface.FromData(data); - _memoryCache.Set(key, typeface, CacheOptions); + memoryCache.Set(key, typeface, CacheOptions); Semaphore.Release(); return typeface; } @@ -88,17 +82,17 @@ private static async Task ReadToSkData(string path) unsafe { - var fileDataBuffer = NativeMemory.Alloc((nuint) fileSize); - fileDataBufferPtr = (nint) fileDataBuffer; + var fileDataBuffer = NativeMemory.Alloc((nuint)fileSize); + fileDataBufferPtr = (nint)fileDataBuffer; fileDataBufferStream = - new UnmanagedMemoryStream((byte*) fileDataBuffer, fileSize, fileSize, FileAccess.ReadWrite); + new UnmanagedMemoryStream((byte*)fileDataBuffer, fileSize, fileSize, FileAccess.ReadWrite); } await fileStream.CopyToAsync(fileDataBufferStream); unsafe { - var data = SKData.Create(fileDataBufferPtr, (int) fileSize, + var data = SKData.Create(fileDataBufferPtr, (int)fileSize, (address, _) => NativeMemory.Free(address.ToPointer())); return data; } diff --git a/appsettings.Development.json b/appsettings.Development.json index 0c208ae..a6e86ac 100644 --- a/appsettings.Development.json +++ b/appsettings.Development.json @@ -1,7 +1,7 @@ { "Logging": { "LogLevel": { - "Default": "Information", + "Default": "Debug", "Microsoft.AspNetCore": "Warning" } }