diff --git a/BotNet.Services/BotCommands/Meme.cs b/BotNet.Services/BotCommands/Meme.cs new file mode 100644 index 0000000..60472a3 --- /dev/null +++ b/BotNet.Services/BotCommands/Meme.cs @@ -0,0 +1,36 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using BotNet.Services.Meme; +using Microsoft.Extensions.DependencyInjection; +using Telegram.Bot; +using Telegram.Bot.Types; +using Telegram.Bot.Types.Enums; +using Telegram.Bot.Types.InputFiles; + +namespace BotNet.Services.BotCommands { + public static class Meme { + public static async Task HandleRamadAsync(ITelegramBotClient botClient, IServiceProvider serviceProvider, Message message, CancellationToken cancellationToken) { + if (message.Entities is { Length: 1 } entities + && entities[0] is { Type: MessageEntityType.BotCommand, Offset: 0, Length: int commandLength } + && message.Text![commandLength..].Trim() is string commandArgument) { + byte[] generatedImage = serviceProvider.GetRequiredService().CaptionRamad(commandArgument); + using MemoryStream floppedImageStream = new(generatedImage); + + await botClient.SendPhotoAsync( + chatId: message.Chat.Id, + photo: new InputOnlineFile(floppedImageStream, "ramad.png"), + replyToMessageId: message.MessageId, + cancellationToken: cancellationToken); + } else { + await botClient.SendTextMessageAsync( + chatId: message.Chat.Id, + text: "Untuk menyuruh Riza presentasi, silakan ketik /ramad diikuti judul presentasi.", + parseMode: ParseMode.Html, + replyToMessageId: message.MessageId, + cancellationToken: cancellationToken); + } + } + } +} diff --git a/BotNet.Services/BotNet.Services.csproj b/BotNet.Services/BotNet.Services.csproj index a5b6d88..4eb8a7f 100644 --- a/BotNet.Services/BotNet.Services.csproj +++ b/BotNet.Services/BotNet.Services.csproj @@ -7,6 +7,7 @@ + @@ -15,6 +16,15 @@ + + + + + + + + + @@ -58,6 +68,16 @@ + + + + + + + + + + diff --git a/BotNet.Services/Meme/Images/Ramad.jpg b/BotNet.Services/Meme/Images/Ramad.jpg new file mode 100644 index 0000000..7046a82 Binary files /dev/null and b/BotNet.Services/Meme/Images/Ramad.jpg differ diff --git a/BotNet.Services/Meme/MemeGenerator.cs b/BotNet.Services/Meme/MemeGenerator.cs new file mode 100644 index 0000000..9d37cc4 --- /dev/null +++ b/BotNet.Services/Meme/MemeGenerator.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using BotNet.Services.Typography; +using SkiaSharp; + +namespace BotNet.Services.Meme { + public class MemeGenerator { + private readonly BotNetFontService _botNetFontService; + + public MemeGenerator( + BotNetFontService botNetFontService + ) { + _botNetFontService = botNetFontService; + } + + public byte[] CaptionRamad(string text) { + using Stream templateStream = typeof(MemeGenerator).Assembly.GetManifestResourceStream(Templates.RAMAD.ImageResourceName)!; + using SKBitmap template = SKBitmap.Decode(templateStream); + using SKSurface surface = SKSurface.Create(new SKImageInfo(template.Width, template.Height)); + using SKCanvas canvas = surface.Canvas; + + SKRect templateRect = SKRect.Create(template.Width, template.Height); + canvas.DrawBitmap( + bitmap: template, + source: templateRect, + dest: templateRect + ); + + canvas.Save(); + canvas.RotateDegrees(1.4f); + using Stream fontStream = _botNetFontService.GetFontStyleById("Inter-Regular").OpenStream(); + using SKTypeface typeface = SKTypeface.FromStream(fontStream); + using SKPaint paint = new() { + TextAlign = SKTextAlign.Left, + Color = new SKColor(0x00, 0x00, 0x00, 0xcc), + Typeface = typeface, + TextSize = 17f, + IsAntialias = true + }; + float offset = 0f; + foreach (string line in WrapWords(text, 110f, paint)) { + canvas.DrawText( + text: line, + x: 120f, + y: 100f + offset, + paint: paint + ); + offset += 20f; // line height + } + canvas.Restore(); + + using SKImage image = surface.Snapshot(); + using SKData data = image.Encode(SKEncodedImageFormat.Png, 100); + + using MemoryStream memoryStream = new(); + data.SaveTo(memoryStream); + + return memoryStream.ToArray(); + } + + private static List WrapWords(string text, float maxWidth, SKPaint paint) { + string[] words = text.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + List lines = new(); + bool firstWord = true; + string line = ""; + foreach (string word in words) { + // Do not wrap first word + if (firstWord) { + line = word; + firstWord = false; + continue; + } + + // Measure how wide will it take if we append this word to the current line + string testLine = line + " " + word; + SKRect bound = new(); + paint.MeasureText(testLine, ref bound); + + // Wrap + if (bound.Width > maxWidth) { + lines.Add(line); + line = word; + continue; + } + + // Append + line += " " + word; + } + lines.Add(line); + + return lines; + } + } +} diff --git a/BotNet.Services/Meme/ServiceCollectionExtensions.cs b/BotNet.Services/Meme/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..520541b --- /dev/null +++ b/BotNet.Services/Meme/ServiceCollectionExtensions.cs @@ -0,0 +1,10 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace BotNet.Services.Meme { + public static class ServiceCollectionExtensions { + public static IServiceCollection AddMemeGenerator(this IServiceCollection services) { + services.AddTransient(); + return services; + } + } +} diff --git a/BotNet.Services/Meme/Templates.cs b/BotNet.Services/Meme/Templates.cs new file mode 100644 index 0000000..23d8586 --- /dev/null +++ b/BotNet.Services/Meme/Templates.cs @@ -0,0 +1,9 @@ +namespace BotNet.Services.Meme { + internal sealed record Template( + string ImageResourceName + ); + + internal static class Templates { + public static readonly Template RAMAD = new("BotNet.Services.Meme.Images.Ramad.jpg"); + } +} diff --git a/BotNet.Services/Typography/Assets/Inter-Black.ttf b/BotNet.Services/Typography/Assets/Inter-Black.ttf new file mode 100644 index 0000000..b27822b Binary files /dev/null and b/BotNet.Services/Typography/Assets/Inter-Black.ttf differ diff --git a/BotNet.Services/Typography/Assets/Inter-Bold.ttf b/BotNet.Services/Typography/Assets/Inter-Bold.ttf new file mode 100644 index 0000000..fe23eeb Binary files /dev/null and b/BotNet.Services/Typography/Assets/Inter-Bold.ttf differ diff --git a/BotNet.Services/Typography/Assets/Inter-ExtraBold.ttf b/BotNet.Services/Typography/Assets/Inter-ExtraBold.ttf new file mode 100644 index 0000000..874b1b0 Binary files /dev/null and b/BotNet.Services/Typography/Assets/Inter-ExtraBold.ttf differ diff --git a/BotNet.Services/Typography/Assets/Inter-ExtraLight.ttf b/BotNet.Services/Typography/Assets/Inter-ExtraLight.ttf new file mode 100644 index 0000000..c993e82 Binary files /dev/null and b/BotNet.Services/Typography/Assets/Inter-ExtraLight.ttf differ diff --git a/BotNet.Services/Typography/Assets/Inter-Light.ttf b/BotNet.Services/Typography/Assets/Inter-Light.ttf new file mode 100644 index 0000000..71188f5 Binary files /dev/null and b/BotNet.Services/Typography/Assets/Inter-Light.ttf differ diff --git a/BotNet.Services/Typography/Assets/Inter-Medium.ttf b/BotNet.Services/Typography/Assets/Inter-Medium.ttf new file mode 100644 index 0000000..a01f377 Binary files /dev/null and b/BotNet.Services/Typography/Assets/Inter-Medium.ttf differ diff --git a/BotNet.Services/Typography/Assets/Inter-Regular.ttf b/BotNet.Services/Typography/Assets/Inter-Regular.ttf new file mode 100644 index 0000000..5e4851f Binary files /dev/null and b/BotNet.Services/Typography/Assets/Inter-Regular.ttf differ diff --git a/BotNet.Services/Typography/Assets/Inter-SemiBold.ttf b/BotNet.Services/Typography/Assets/Inter-SemiBold.ttf new file mode 100644 index 0000000..ecc7041 Binary files /dev/null and b/BotNet.Services/Typography/Assets/Inter-SemiBold.ttf differ diff --git a/BotNet.Services/Typography/Assets/Inter-Thin.ttf b/BotNet.Services/Typography/Assets/Inter-Thin.ttf new file mode 100644 index 0000000..fe77243 Binary files /dev/null and b/BotNet.Services/Typography/Assets/Inter-Thin.ttf differ diff --git a/BotNet.Services/Typography/BotNetFontService.cs b/BotNet.Services/Typography/BotNetFontService.cs index 3ca063a..489bd99 100644 --- a/BotNet.Services/Typography/BotNetFontService.cs +++ b/BotNet.Services/Typography/BotNetFontService.cs @@ -9,6 +9,10 @@ public class BotNetFontService { name: "JetBrainsMonoNL", stylesSetup: EnumerateJetBrainsMonoMLStyles); + private readonly FontFamily _inter = new( + name: "Inter", + stylesSetup: EnumerateInterStyles); + private static IEnumerable EnumerateJetBrainsMonoMLStyles(FontFamily fontFamily) { Assembly resourceAssembly = Assembly.GetAssembly(typeof(BotNetFontService))!; string resourceNamespace = "BotNet.Services.Typography.Assets"; @@ -43,8 +47,39 @@ FontStyle CreateFontStyle(string name, int weight, FontStyleType styleType) { yield return CreateFontStyle("JetBrainsMonoNL-ExtraBoldItalic", 800, FontStyleType.Italic); } + private static IEnumerable EnumerateInterStyles(FontFamily fontFamily) { + Assembly resourceAssembly = Assembly.GetAssembly(typeof(BotNetFontService))!; + string resourceNamespace = "BotNet.Services.Typography.Assets"; + + FontStyle CreateFontStyle(string name, int weight, FontStyleType styleType) { + return new FontStyle( + id: name, + name: name, + fullName: name, + weight: weight, + styleType: styleType, + fontFamily: fontFamily, + resourceAssembly: resourceAssembly, + resourceName: $"{resourceNamespace}.{name}.ttf"); + } + + yield return CreateFontStyle("Inter-Thin", 100, FontStyleType.Normal); + yield return CreateFontStyle("Inter-ExtraLight", 200, FontStyleType.Normal); + yield return CreateFontStyle("Inter-Light", 300, FontStyleType.Normal); + yield return CreateFontStyle("Inter-Regular", 400, FontStyleType.Normal); + yield return CreateFontStyle("Inter-Medium", 500, FontStyleType.Normal); + yield return CreateFontStyle("Inter-SemiBold", 600, FontStyleType.Normal); + yield return CreateFontStyle("Inter-Bold", 700, FontStyleType.Normal); + yield return CreateFontStyle("Inter-ExtraBold", 800, FontStyleType.Normal); + yield return CreateFontStyle("Inter-Black", 900, FontStyleType.Normal); + } + public FontStyle GetDefaultFontStyle() => _jetbrainsMonoNL.GetFontStyles().Single(style => style is { Weight: 400, StyleType: FontStyleType.Normal }); - public FontFamily[] GetFontFamilies() => new[] { _jetbrainsMonoNL }; - public FontStyle GetFontStyleById(string id) => _jetbrainsMonoNL.GetFontStyles().Single(style => style.Id == id); + public FontFamily[] GetFontFamilies() => new[] { _jetbrainsMonoNL, _inter }; + + public FontStyle GetFontStyleById(string id) + => _jetbrainsMonoNL.GetFontStyles().SingleOrDefault(style => style.Id == id) + ?? _inter.GetFontStyles().SingleOrDefault(style => style.Id == id) + ?? throw new KeyNotFoundException(); } } diff --git a/BotNet/Bot/UpdateHandler.cs b/BotNet/Bot/UpdateHandler.cs index 3749d43..1ea2d42 100644 --- a/BotNet/Bot/UpdateHandler.cs +++ b/BotNet/Bot/UpdateHandler.cs @@ -299,6 +299,9 @@ await botClient.SendTextMessageAsync( case "/preview": await Preview.GetPreviewAsync(botClient, _serviceProvider, update.Message, cancellationToken); break; + case "/ramad": + await Meme.HandleRamadAsync(botClient, _serviceProvider, update.Message, cancellationToken); + break; } } break; diff --git a/BotNet/Controllers/MemeGeneratorTestController.cs b/BotNet/Controllers/MemeGeneratorTestController.cs new file mode 100644 index 0000000..6082388 --- /dev/null +++ b/BotNet/Controllers/MemeGeneratorTestController.cs @@ -0,0 +1,23 @@ +#if DEBUG +using BotNet.Services.Meme; +using Microsoft.AspNetCore.Mvc; + +namespace BotNet.Controllers { + [Route("meme")] + public class MemeGeneratorTestController : Controller { + private readonly MemeGenerator _memeGenerator; + + public MemeGeneratorTestController( + MemeGenerator memeGenerator + ) { + _memeGenerator = memeGenerator; + } + + [Route("ramad")] + public IActionResult RenderRamad() { + byte[] memePng = _memeGenerator.CaptionRamad("Melakukan TDD, meski situasi sulit"); + return File(memePng, "image/png", true); + } + } +} +#endif diff --git a/BotNet/Program.cs b/BotNet/Program.cs index 3286410..6785e52 100644 --- a/BotNet/Program.cs +++ b/BotNet/Program.cs @@ -27,6 +27,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Orleans.Hosting; +using BotNet.Services.Meme; Host.CreateDefaultBuilder(args) .ConfigureWebHostDefaults(webBuilder => webBuilder.UseStartup()) @@ -73,6 +74,7 @@ services.AddWeatherService(); services.AddBMKG(); services.AddPreviewServices(); + services.AddMemeGenerator(); // Hosted Services services.Configure(configuration.GetSection("BotOptions"));