From ef8a2cb8650cf692641df296e93c1c1f5a817466 Mon Sep 17 00:00:00 2001 From: Slavasil Date: Sun, 20 Apr 2025 00:39:56 +0300 Subject: [PATCH] implement demotivator generation --- SimpleTGBot/MemeGen/DemotivatorGen.cs | 269 ++++++++++++++++++++++++++ SimpleTGBot/MemeGen/Types.cs | 23 +++ SimpleTGBot/Program.cs | 9 +- SimpleTGBot/SimpleTGBot.csproj | 3 +- SimpleTGBot/TelegramBot.cs | 7 +- 5 files changed, 308 insertions(+), 3 deletions(-) create mode 100644 SimpleTGBot/MemeGen/DemotivatorGen.cs create mode 100644 SimpleTGBot/MemeGen/Types.cs diff --git a/SimpleTGBot/MemeGen/DemotivatorGen.cs b/SimpleTGBot/MemeGen/DemotivatorGen.cs new file mode 100644 index 0000000..b1c4222 --- /dev/null +++ b/SimpleTGBot/MemeGen/DemotivatorGen.cs @@ -0,0 +1,269 @@ +using System.Drawing; +using System.Drawing.Imaging; +using System.Drawing.Text; +using System.Text; + +namespace SimpleTGBot.MemeGen; + +public class DemotivatorGen +{ + private static FontFamily defaultFontFamily; + + public DemotivatorGen() + { + + } + + static DemotivatorGen() + { + try + { + defaultFontFamily = new FontFamily("DejaVu Serif"); + } + catch + { + defaultFontFamily = new FontFamily(GenericFontFamilies.SansSerif); + } + } + + public static DemotivatorStyle DefaultStyle() + { + return new DemotivatorStyle() + { + BorderThickness = 6, + Padding = 10, + OuterMargin = 20, + CaptionSpacing = 32, + Wtf1 = 60, + OutlineColor = Color.FromArgb(255, 255, 255, 255), + TitleColor = Color.FromArgb(255, 255, 255, 255), + SubtitleColor = Color.FromArgb(255, 255, 255, 255), + BackgroundColor = Color.FromArgb(255, 0, 0, 0), + TitleFont = new Font(defaultFontFamily, 50), + SubtitleFont = new Font(defaultFontFamily, 25), + }; + } + +#pragma warning disable CA1416 // мы точно работаем под Windows 7+ (проверено в Program.Main) + public static MemoryStream MakePictureDemotivator(string picturePath, DemotivatorText[] texts, DemotivatorStyle style) + { + Bitmap picture = new Bitmap(picturePath); + // данная bitmap предназначена только для подсчёта размера текста + Bitmap bitmap = new Bitmap(1, 1); + Graphics g = Graphics.FromImage(bitmap); + Func measureTitleString = s => g.MeasureString(s, style.TitleFont).Width; + Func measureSubtitleString = s => g.MeasureString(s, style.SubtitleFont).Width; + + // код расчёта + SizeF[] subdemSizes = new SizeF[texts.Length]; + float aspectRatio = (float)picture.Height / picture.Width; + Console.WriteLine("aspect ratio " + aspectRatio); + float scaledPictureWidth = Math.Clamp(picture.Width, 800, 1200); + float scaledPictureHeight = scaledPictureWidth * aspectRatio; + float contentWidth = scaledPictureWidth + style.Padding * 2; + float contentHeight = scaledPictureHeight + style.Padding * 2; + float titleFontHeight = style.TitleFont.GetHeight(); + float subtitleFontHeight = style.SubtitleFont.GetHeight(); + string[][] titles = new string[texts.Length][]; + string[][] subtitles = new string[texts.Length][]; + for (int i = 0; i < texts.Length; ++i) + { + Console.WriteLine("subdemotivator #" + i); + float frameWidth = contentWidth + style.Padding * 2.0f + style.BorderThickness * 2.0f; + Console.WriteLine("frame width " + frameWidth); + float frameHeight = contentHeight + style.Padding * 2.0f + style.BorderThickness * 2.0f; + + string title = texts[i].Title; + WordWrapResult titleWrap = wordWrap(title, frameWidth + style.Wtf1 * 2f, frameWidth * 1.5f, measureTitleString); + // captions.set(i * 2, titleWrap.wrappedString) + titles[i] = titleWrap.lines; + Console.WriteLine("title width " + titleWrap.actualWidth); + + string subtitle = texts[i].Subtitle; + WordWrapResult subtitleWrap = wordWrap(subtitle, frameWidth + style.Wtf1 * 2f, titleWrap.actualWidth, measureSubtitleString); + // captions.set(i * 2 + 1, subtitleWrap.wrappedString) + subtitles[i] = subtitleWrap.lines; + Console.WriteLine("subtitle width " + subtitleWrap.actualWidth); + + float subdemWidth = Math.Max(frameWidth, Math.Max(titleWrap.actualWidth, subtitleWrap.actualWidth)); + subdemWidth += style.Padding * 2f; + Console.WriteLine("subdemotivator width: " + subdemWidth); + + // Console.WriteLine("\"" + titleWrap.wrappedString + "\""); + // Console.WriteLine("\"" + subtitleWrap.wrappedString + "\""); + int titleLineCount = titleWrap.lines.Length; + int subtitleLineCount = subtitleWrap.lines.Length; + Console.WriteLine(titleLineCount + " lines of title, " + subtitleLineCount + " lines of subtitle"); + float titleHeight = titleFontHeight * titleLineCount; + float subtitleHeight = subtitleFontHeight * subtitleLineCount; + + float subdemHeight = style.Padding * 2f + frameHeight + titleHeight + subtitleHeight; + subdemSizes[i] = new SizeF(subdemWidth, subdemHeight); + Console.WriteLine("subdemotivator size: " + subdemWidth + "x" + subdemHeight); + contentWidth = subdemWidth; + contentHeight = subdemHeight; + Console.WriteLine("-------------------------"); + } + contentHeight += style.OuterMargin * 2f; + + g.Dispose(); + bitmap = new Bitmap((int)contentWidth + (int)style.OuterMargin * 2, (int)contentHeight + (int)style.OuterMargin * 2); + g = Graphics.FromImage(bitmap); + + // код рисования + SolidBrush backgroundBrush = new SolidBrush(style.BackgroundColor); + SolidBrush outlineBrush = new SolidBrush(style.OutlineColor); + SolidBrush titleBrush = new SolidBrush(style.TitleColor); + SolidBrush subtitleBrush = new SolidBrush(style.SubtitleColor); + g.FillRectangle(backgroundBrush, 0, 0, bitmap.Width, bitmap.Height); + + PointF currentOrigin = new PointF(style.OuterMargin, style.OuterMargin); + for (int j = texts.Length - 1; j >= 0; --j) + { + float contWidth = j != 0 ? subdemSizes[j - 1].Width : (scaledPictureWidth + style.Padding * 2f); + float contHeight = j != 0 ? subdemSizes[j - 1].Height : (scaledPictureHeight + style.Padding * 2f); + + float availableWidth = subdemSizes[j].Width; + float contX = currentOrigin.X + (availableWidth - contWidth) / 2f; + float contY = currentOrigin.Y + style.Padding * 2f; + + float bt = style.BorderThickness; + float pad = style.Padding; + float capSp = style.CaptionSpacing; + + g.FillRectangle(outlineBrush, contX, contY, contWidth, contHeight); + g.FillRectangle(backgroundBrush, contX + bt, contY + bt, contWidth - bt * 2f, contHeight - bt * 2f); + if (j == 0) + { + Console.WriteLine($"drawing image at ({contX+bt+pad}; {contY+bt+pad}) with size {contWidth-bt*2-pad*2}x{contHeight-bt*2-pad*2}"); + g.DrawImage(picture, contX + bt + pad, contY + bt + pad, contWidth - bt * 2 - pad * 2, contHeight - bt * 2 - pad * 2); + } + + float titleY = contY + contHeight + capSp;// + style.TitleFont.GetHeight(); + foreach (string titleLine in titles[j]) + { + float titleX = currentOrigin.X + (availableWidth - g.MeasureString(titleLine, style.TitleFont).Width) / 2f; + g.DrawString(titleLine, style.TitleFont, titleBrush, titleX, titleY); + titleY += titleFontHeight; + } + + float subtitleY = titleY/* - titleFontHeight*/ + capSp; + string[] subtitleLines = subtitles[j]; + if (subtitleLines.Length > 0) + { + // subtitleY += subtitleFontHeight; + foreach (string subtitleLine in subtitleLines) + { + float subtitleX = currentOrigin.X + (availableWidth - measureSubtitleString(subtitleLine)) / 2f; + g.DrawString(subtitleLine, style.SubtitleFont, subtitleBrush, subtitleX, subtitleY); + subtitleY += subtitleFontHeight; + } + } + Console.WriteLine("sub-demotivator " + j + " height is " + (subtitleY - currentOrigin.Y)); + currentOrigin.X += bt + (availableWidth - contWidth) / 2f; + currentOrigin.Y += bt + pad; + } + + MemoryStream outStream = new MemoryStream(); + bitmap.Save(outStream, ImageFormat.Png); + outStream.Seek(0, SeekOrigin.Begin); + return outStream; + } + + private static WordWrapResult wordWrap(string rawText, float width, float maxWidth, Func measureString) + { + float free = width; + StringBuilder wrappedText = new StringBuilder(); + int words = (rawText.Length != 0) ? (rawText.Count(c => c == ' ') + 1) : 0; + int rawPosition = 0; + float actualWidth = 0; + bool trailingReturn = false; + while (rawPosition < rawText.Length) + { + string word = takeWord(rawText, rawPosition); + float wordWidth = measureString(word + ' '); + if (wordWidth <= free) + { + wrappedText.Append(word); + wrappedText.Append(' '); + trailingReturn = false; + free -= wordWidth; + actualWidth = Math.Max(width - free, actualWidth); + rawPosition += word.Length + 1; + words--; + continue; // TODO заменить if на else-if чтобы убрать continue + } + if (wordWidth <= width) + { + if (!trailingReturn) + wrappedText.Append("\n"); + wrappedText.Append(word); + wrappedText.Append(" "); + trailingReturn = false; + free = width - wordWidth; + rawPosition += word.Length + 1; + words--; + continue; + } + if (wordWidth <= maxWidth) + { + actualWidth = Math.Max(actualWidth, wordWidth); + if (!trailingReturn) + wrappedText.Append("\n"); + wrappedText.Append(word); + wrappedText.Append("\n"); + trailingReturn = true; + free = width; + rawPosition += word.Length + 1; + words--; + continue; + } + + // TODO заменить на что-то поэффективнее + float substrWidth = 0; + int substrLength = word.Length; + for (int c = 0; c < word.Length; ++c) + { + float charWidth = measureString(word[c].ToString()); + if (substrWidth + charWidth > maxWidth) + { + substrLength = c; + break; + } + substrWidth += charWidth; + } + if (!trailingReturn) + wrappedText.Append("\n"); + wrappedText.Append(word.Substring(0, substrLength)); + wrappedText.Append("\n"); + trailingReturn = true; + free = width; + rawPosition += substrLength; + actualWidth = maxWidth; + } + return new WordWrapResult() + { + lines = wrappedText.ToString().Split('\n'), + actualWidth = actualWidth, + }; + } + + private static string takeWord(string text, int position) + { + for (int i = position; i < text.Length; i++) + { + char c = text[i]; + if (c == ' ') + { + return text.Substring(position, i - position); + } + } + return text.Substring(position); + } + + struct WordWrapResult + { + public string[] lines; + public float actualWidth; + } +} diff --git a/SimpleTGBot/MemeGen/Types.cs b/SimpleTGBot/MemeGen/Types.cs new file mode 100644 index 0000000..5cf6e27 --- /dev/null +++ b/SimpleTGBot/MemeGen/Types.cs @@ -0,0 +1,23 @@ +using System.Drawing; + +namespace SimpleTGBot.MemeGen; + +public record DemotivatorText { + public string Title { get; init; } + public string Subtitle { get; init; } +} + +public record DemotivatorStyle +{ + public float BorderThickness { get; set; } + public float Padding { get; set; } + public float OuterMargin { get; set; } + public float CaptionSpacing { get; set; } + public float Wtf1 { get; set; } + public Color OutlineColor { get; set; } + public Color TitleColor { get; set; } + public Color SubtitleColor { get; set; } + public Color BackgroundColor { get; set; } + public Font TitleFont { get; set; } + public Font SubtitleFont { get; set; } +} diff --git a/SimpleTGBot/Program.cs b/SimpleTGBot/Program.cs index 730c2ce..dcd4aa1 100644 --- a/SimpleTGBot/Program.cs +++ b/SimpleTGBot/Program.cs @@ -5,8 +5,13 @@ namespace SimpleTGBot; public static class Program { // Метод main немного видоизменился для асинхронной работы - public static async Task Main(string[] args) + public static async Task Main(string[] args) { + if (Environment.OSVersion.Platform != PlatformID.Win32NT || Environment.OSVersion.Version < new Version(6, 1)) + { + Console.WriteLine("К сожалению, из-за используемых графических функций бот поддерживает только Windows начиная с версии 7."); + return 8; + } // Православная кодировка Console.OutputEncoding = Encoding.UTF8; @@ -26,5 +31,7 @@ public static class Program TelegramBot telegramBot = new TelegramBot(botToken); await telegramBot.Run(); + + return 0; } } diff --git a/SimpleTGBot/SimpleTGBot.csproj b/SimpleTGBot/SimpleTGBot.csproj index e2c073c..ed4a1e7 100644 --- a/SimpleTGBot/SimpleTGBot.csproj +++ b/SimpleTGBot/SimpleTGBot.csproj @@ -1,4 +1,4 @@ - + Exe @@ -8,6 +8,7 @@ + diff --git a/SimpleTGBot/TelegramBot.cs b/SimpleTGBot/TelegramBot.cs index 815dc49..dcc4b8c 100644 --- a/SimpleTGBot/TelegramBot.cs +++ b/SimpleTGBot/TelegramBot.cs @@ -1,4 +1,5 @@ -using Telegram.Bot; +using SimpleTGBot.MemeGen; +using Telegram.Bot; using Telegram.Bot.Exceptions; using Telegram.Bot.Polling; using Telegram.Bot.Types; @@ -76,6 +77,10 @@ botQuit: chatId: chatId, text: "Ты написал:\n" + messageText, cancellationToken: cancellationToken); + + // грязный тест + MemoryStream demotivatorPng = DemotivatorGen.MakePictureDemotivator("pic.png", [new DemotivatorText() {Title=messageText, Subtitle=messageText}], DemotivatorGen.DefaultStyle()); + await botClient.SendPhotoAsync(message.Chat.Id, new InputFile(demotivatorPng, "dem.png")); } ///