diff --git a/TagCloudConsoleClient/ConsoleClient.cs b/TagCloudConsoleClient/ConsoleClient.cs new file mode 100644 index 00000000..51cc891c --- /dev/null +++ b/TagCloudConsoleClient/ConsoleClient.cs @@ -0,0 +1,92 @@ +using CommandLine; +using System.Drawing; +using System.Drawing.Imaging; +using TagCloudGenerator.Core.Interfaces; +using TagCloudGenerator.Core.Models; +using TagCloudGenerator.Infrastructure; +using static System.Runtime.InteropServices.JavaScript.JSType; + +namespace TagCloudConsoleClient +{ + public class ConsoleClient : IClient + { + private readonly ITagCloudGenerator _generator; + private readonly IEnumerable _filters; + private readonly IReaderRepository _readerRepository; + private readonly INormalizer _normalizer; + + public ConsoleClient(ITagCloudGenerator generator, IEnumerable filters, IReaderRepository readers, INormalizer normalizer) + { + _generator = generator; + _filters = filters; + _readerRepository = readers; + _normalizer = normalizer; + } + + public void Run(string[] args) + { + Parser.Default.ParseArguments(args) + .WithParsed(RunWithOptions) + .WithNotParsed(errors => { }); + } + + private void RunWithOptions(Options opts) + { + if (!File.Exists(opts.InputFile)) + { + Console.WriteLine($"Error: input file '{opts.InputFile}' does not exist."); + return; + } + + var canvasSettings = new CanvasSettings() + .SetSize(opts.Width, opts.Height) + .SetBackgroundColor(TryParseColor(opts.BackgroundColor)) + .WithShowRectangles() + .SetPadding(opts.Padding); + + var textSettings = new TextSettings() + .SetFontFamily(opts.FontFamily) + .SetFontSizeRange(opts.MinFontSize, opts.MaxFontSize) + .SetTextColor(TryParseColor(opts.TextColor)); + + string inputFile = opts.InputFile; + string outputFile = opts.OutputFile; + + Console.WriteLine("Starting tag cloud generation..."); + Console.WriteLine($"Input file: {inputFile}"); + Console.WriteLine($"Output file: {outputFile}"); + + + _readerRepository.TryGetReader(inputFile) + .Then(reader => reader.TryRead(inputFile)) + .Then(words => _normalizer.Normalize(words)) + .Then(words => _generator.Generate(words, canvasSettings, textSettings, _filters)) + .Then(image => Result.OfAction(() => image.Save(outputFile, ImageFormat.Png), "Failed to save output image")) + .OnFail(error => + { + Console.WriteLine("Error during generation:"); + Console.WriteLine(error); + }); + } + + private static Color? TryParseColor(string colorStr) + { + if (string.IsNullOrWhiteSpace(colorStr)) + return null; + + try + { + var known = Color.FromName(colorStr); + if (known.IsKnownColor) + return known; + + return ColorTranslator.FromHtml(colorStr); + } + catch + { + Console.WriteLine($"Color '{colorStr}' could not be parsed. Default color will be used."); + return null; + } + } + } +} diff --git a/TagCloudConsoleClient/Options.cs b/TagCloudConsoleClient/Options.cs new file mode 100644 index 00000000..2c5220cf --- /dev/null +++ b/TagCloudConsoleClient/Options.cs @@ -0,0 +1,41 @@ +using CommandLine; + +namespace TagCloudConsoleClient +{ + public class Options + { + [Value(0, MetaName = "input", + HelpText = "Path to the input file containing text.", + Required = true)] + public string InputFile { get; set; } + + [Value(1, MetaName = "output", + HelpText = "Path for the output image (e.g., out.png).", + Required = true)] + public string OutputFile { get; set; } + + [Option("width", HelpText = "Canvas width (default is 1000).")] + public int? Width { get; set; } + + [Option("height", HelpText = "Canvas height (default is 1000).")] + public int? Height { get; set; } + + [Option("padding", HelpText = "Canvas padding (default if 50).")] + public int? Padding { get; set; } + + [Option("bgcolor", HelpText = "Background color (name or #RRGGBB).")] + public string BackgroundColor { get; set; } + + [Option("textcolor", HelpText = "Text color (name or #RRGGBB).")] + public string TextColor { get; set; } + + [Option("font", HelpText = "Font family name (e.g., Arial).")] + public string FontFamily { get; set; } + + [Option("minsize", HelpText = "Minimum font size.")] + public float? MinFontSize { get; set; } + + [Option("maxsize", HelpText = "Maximum font size.")] + public float? MaxFontSize { get; set; } + } +} diff --git a/TagCloudConsoleClient/Program.cs b/TagCloudConsoleClient/Program.cs new file mode 100644 index 00000000..4944c8f9 --- /dev/null +++ b/TagCloudConsoleClient/Program.cs @@ -0,0 +1,25 @@ +using Autofac; +using TagCloudGenerator.Core.Interfaces; +using TagCloudGenerator.DI; +using TagCloudGenerator.Infrastructure.Filters; + +namespace TagCloudConsoleClient +{ + public class Program + { + public static void Main(string[] args) + { + var builder = new ContainerBuilder(); + + builder.RegisterModule(); + builder.RegisterType().As(); + builder.RegisterType().As(); + + var container = builder.Build(); + + using var scope = container.BeginLifetimeScope(); + var client = scope.Resolve(); + client.Run(args); + } + } +} \ No newline at end of file diff --git a/TagCloudConsoleClient/TagCloudConsoleClient.csproj b/TagCloudConsoleClient/TagCloudConsoleClient.csproj new file mode 100644 index 00000000..f4c4f3d1 --- /dev/null +++ b/TagCloudConsoleClient/TagCloudConsoleClient.csproj @@ -0,0 +1,14 @@ + + + + Exe + net8.0-windows + enable + disable + + + + + + + diff --git a/TagCloudGenerator/Algorithms/BasicTagCloudAlgorithm.cs b/TagCloudGenerator/Algorithms/BasicTagCloudAlgorithm.cs new file mode 100644 index 00000000..403a5bf1 --- /dev/null +++ b/TagCloudGenerator/Algorithms/BasicTagCloudAlgorithm.cs @@ -0,0 +1,100 @@ +using System.Drawing; +using TagCloudGenerator.Core.Interfaces; + +namespace TagCloudGenerator.Algorithms +{ + public class BasicTagCloudAlgorithm : ITagCloudAlgorithm + { + private Point center; + + private readonly Random random = new Random(); + + private readonly List rectangles = new List(); + + private double currentAngle = 0; + private double currentRadius = 0; + + private int countToGenerate = 10; + + private (int min, int max) width = new(20, 21); + private (int min, int max) height = new(20, 21); + + private double angleStep = 0.1; + private double radiusStep = 0.5; + + public BasicTagCloudAlgorithm(Point center) + { + if (center.X <= 0 || center.Y <= 0) throw new ArgumentException("Center coordinates must be positives"); + this.center = center; + } + + public BasicTagCloudAlgorithm() + { + this.center = new Point(0, 0); + } + + public Rectangle PutNextRectangle(Size rectangleSize) + { + if (rectangleSize.Width <= 0 || rectangleSize.Height <= 0) throw new ArgumentException("Rectangle sizes must be positives"); + + if (rectangles.Count == 0) + { + return PutFirstRectangle(rectangleSize); + } + + return PlaceNextRectange(rectangleSize); + } + + private Rectangle PlaceNextRectange(Size rectangleSize) + { + while (true) + { + double x = center.X + currentRadius * Math.Cos(currentAngle); + double y = center.Y + currentRadius * Math.Sin(currentAngle); + + var potentialCenter = new Point((int)(x - rectangleSize.Width / 2), (int)(y - rectangleSize.Height / 2)); + var potentialRectangle = new Rectangle(potentialCenter, rectangleSize); + + if (!IntersectWithAny(potentialRectangle)) + { + rectangles.Add(potentialRectangle); + return potentialRectangle; + } + + currentAngle += angleStep; + if (currentAngle >= 2 * Math.PI) + { + currentAngle = 0; + currentRadius += radiusStep; + } + } + } + + private bool IntersectWithAny(Rectangle potentialRectangle) + { + return rectangles.Any(rect => rect.IntersectsWith(potentialRectangle)); + } + + private Rectangle PutFirstRectangle(Size rectangleSize) + { + var firstCenter = new Point((int)(center.X - rectangleSize.Width / 2), (int)(center.Y - rectangleSize.Width / 2)); + var first = new Rectangle(firstCenter, rectangleSize); + currentRadius = rectangleSize.Height / 2; + rectangles.Add(first); + return first; + } + + public BasicTagCloudAlgorithm WithCenterAt(Point point) + { + if (point.X <= 0 || point.Y <= 0) throw new ArgumentException("Center coordinates must be positives"); + center = point; + return this; + } + + public ITagCloudAlgorithm Reset() + { + rectangles.Clear(); + return this; + } + } +} diff --git a/TagCloudGenerator/Core/Interfaces/IAnalyzer.cs b/TagCloudGenerator/Core/Interfaces/IAnalyzer.cs new file mode 100644 index 00000000..8c47d59d --- /dev/null +++ b/TagCloudGenerator/Core/Interfaces/IAnalyzer.cs @@ -0,0 +1,9 @@ +using TagCloudGenerator.Core.Models; + +namespace TagCloudGenerator.Core.Interfaces +{ + public interface IAnalyzer + { + Dictionary Analyze(List words); + } +} diff --git a/TagCloudGenerator/Core/Interfaces/IClient.cs b/TagCloudGenerator/Core/Interfaces/IClient.cs new file mode 100644 index 00000000..de0e1098 --- /dev/null +++ b/TagCloudGenerator/Core/Interfaces/IClient.cs @@ -0,0 +1,7 @@ +namespace TagCloudGenerator.Core.Interfaces +{ + public interface IClient + { + void Run(string[] args); + } +} diff --git a/TagCloudGenerator/Core/Interfaces/IFilter.cs b/TagCloudGenerator/Core/Interfaces/IFilter.cs new file mode 100644 index 00000000..e660e782 --- /dev/null +++ b/TagCloudGenerator/Core/Interfaces/IFilter.cs @@ -0,0 +1,8 @@ + +namespace TagCloudGenerator.Core.Interfaces +{ + public interface IFilter + { + List Filter(List words); + } +} diff --git a/TagCloudGenerator/Core/Interfaces/IFontSizeCalculator.cs b/TagCloudGenerator/Core/Interfaces/IFontSizeCalculator.cs new file mode 100644 index 00000000..57033c7e --- /dev/null +++ b/TagCloudGenerator/Core/Interfaces/IFontSizeCalculator.cs @@ -0,0 +1,8 @@ + +namespace TagCloudGenerator.Core.Interfaces +{ + public interface IFontSizeCalculator + { + float Calculate(int wordFrequency, int minFrequency, int maxFrequency, float minFontSize, float maxFontSize); + } +} diff --git a/TagCloudGenerator/Core/Interfaces/IFormatReader.cs b/TagCloudGenerator/Core/Interfaces/IFormatReader.cs new file mode 100644 index 00000000..0575196d --- /dev/null +++ b/TagCloudGenerator/Core/Interfaces/IFormatReader.cs @@ -0,0 +1,10 @@ +using TagCloudGenerator.Infrastructure; + +namespace TagCloudGenerator.Core.Interfaces +{ + public interface IFormatReader + { + bool CanRead(string filePath); + Result> TryRead(string filePath); + } +} diff --git a/TagCloudGenerator/Core/Interfaces/INormalizer.cs b/TagCloudGenerator/Core/Interfaces/INormalizer.cs new file mode 100644 index 00000000..daaaf97b --- /dev/null +++ b/TagCloudGenerator/Core/Interfaces/INormalizer.cs @@ -0,0 +1,7 @@ +namespace TagCloudGenerator.Core.Interfaces +{ + public interface INormalizer + { + public List Normalize(List words); + } +} diff --git a/TagCloudGenerator/Core/Interfaces/IReaderRepository.cs b/TagCloudGenerator/Core/Interfaces/IReaderRepository.cs new file mode 100644 index 00000000..83f5ba47 --- /dev/null +++ b/TagCloudGenerator/Core/Interfaces/IReaderRepository.cs @@ -0,0 +1,9 @@ +using TagCloudGenerator.Infrastructure; + +namespace TagCloudGenerator.Core.Interfaces +{ + public interface IReaderRepository + { + public Result TryGetReader(string filePath); + } +} \ No newline at end of file diff --git a/TagCloudGenerator/Core/Interfaces/IRenderer.cs b/TagCloudGenerator/Core/Interfaces/IRenderer.cs new file mode 100644 index 00000000..b4eea0d2 --- /dev/null +++ b/TagCloudGenerator/Core/Interfaces/IRenderer.cs @@ -0,0 +1,11 @@ +using System.Drawing; +using TagCloudGenerator.Core.Models; +using TagCloudGenerator.Infrastructure; + +namespace TagCloudGenerator.Core.Interfaces +{ + public interface IRenderer + { + public Result Render(IEnumerable items, CanvasSettings canvasSettings, TextSettings textSettings); + } +} diff --git a/TagCloudGenerator/Core/Interfaces/ISorterer.cs b/TagCloudGenerator/Core/Interfaces/ISorterer.cs new file mode 100644 index 00000000..b504e89d --- /dev/null +++ b/TagCloudGenerator/Core/Interfaces/ISorterer.cs @@ -0,0 +1,8 @@ + +namespace TagCloudGenerator.Core.Interfaces +{ + public interface ISorterer + { + public List<(string Word, int Frequency)> Sort(Dictionary wordsWithFreqs); + } +} diff --git a/TagCloudGenerator/Core/Interfaces/ITagCloudAlgorithm.cs b/TagCloudGenerator/Core/Interfaces/ITagCloudAlgorithm.cs new file mode 100644 index 00000000..1331e9b8 --- /dev/null +++ b/TagCloudGenerator/Core/Interfaces/ITagCloudAlgorithm.cs @@ -0,0 +1,12 @@ +using System.Drawing; +using TagCloudGenerator.Algorithms; + +namespace TagCloudGenerator.Core.Interfaces +{ + public interface ITagCloudAlgorithm + { + Rectangle PutNextRectangle(Size rectangleSize); + + public ITagCloudAlgorithm Reset(); + } +} diff --git a/TagCloudGenerator/Core/Interfaces/ITagCloudGenerator.cs b/TagCloudGenerator/Core/Interfaces/ITagCloudGenerator.cs new file mode 100644 index 00000000..429ab2f4 --- /dev/null +++ b/TagCloudGenerator/Core/Interfaces/ITagCloudGenerator.cs @@ -0,0 +1,11 @@ +using System.Drawing; +using TagCloudGenerator.Core.Models; +using TagCloudGenerator.Infrastructure; + +namespace TagCloudGenerator.Core.Interfaces +{ + public interface ITagCloudGenerator + { + public Result Generate(List words, CanvasSettings canvasSettings, TextSettings textSettings, IEnumerable filters); + } +} diff --git a/TagCloudGenerator/Core/Interfaces/ITextMeasurer.cs b/TagCloudGenerator/Core/Interfaces/ITextMeasurer.cs new file mode 100644 index 00000000..0f1f73be --- /dev/null +++ b/TagCloudGenerator/Core/Interfaces/ITextMeasurer.cs @@ -0,0 +1,9 @@ +using System.Drawing; + +namespace TagCloudGenerator.Core.Interfaces +{ + public interface ITextMeasurer + { + Size Measure(string word, float fontSize, string fontFamily); + } +} diff --git a/TagCloudGenerator/Core/Models/CanvasSettings.cs b/TagCloudGenerator/Core/Models/CanvasSettings.cs new file mode 100644 index 00000000..2f4beafa --- /dev/null +++ b/TagCloudGenerator/Core/Models/CanvasSettings.cs @@ -0,0 +1,62 @@ +using System.Drawing; + +namespace TagCloudGenerator.Core.Models +{ + public class CanvasSettings + { + public Color BackgroundColor { get; private set; } = Color.White; + public Size CanvasSize { get; private set; } = new Size(1000, 1000); + public bool ShowRectangles { get; private set; } = true; + public int EdgePadding { get; private set; } = 50; + + public CanvasSettings SetWidth(int? width) + { + var size = CanvasSize; + + size.Width = width is > 0 ? width.Value : CanvasSize.Width; + CanvasSize = size; + + return this; + } + + public CanvasSettings SetHeight(int? height) + { + var size = CanvasSize; + + size.Height = height is > 0 ? height.Value : CanvasSize.Height; + CanvasSize = size; + + return this; + } + + public CanvasSettings SetSize(int? width, int? height) + { + var size = CanvasSize; + + size.Width = width is > 0 ? width.Value : CanvasSize.Width; + size.Height = height is > 0 ? height.Value : CanvasSize.Height; + CanvasSize = size; + + return this; + } + + public CanvasSettings SetBackgroundColor(Color? color) + { + if (color.HasValue) + BackgroundColor = color.Value; + return this; + } + + public CanvasSettings WithShowRectangles(bool value = true) + { + ShowRectangles = value; + return this; + } + + public CanvasSettings SetPadding(int? padding) + { + EdgePadding = padding is >= 0 ? (int)padding.Value : EdgePadding; + return this; + } + } +} diff --git a/TagCloudGenerator/Core/Models/CloudItem.cs b/TagCloudGenerator/Core/Models/CloudItem.cs new file mode 100644 index 00000000..c6a00eea --- /dev/null +++ b/TagCloudGenerator/Core/Models/CloudItem.cs @@ -0,0 +1,49 @@ +using System.Drawing; + +namespace TagCloudGenerator.Core.Models +{ + public class CloudItem + { + public string Word { get; } + public Rectangle Rectangle { get; } + public float FontSize { get; } + public Color? TextColor { get; } + public string FontFamily { get; } + public FontStyle FontStyle { get; } + public int Frequency { get; } + + public CloudItem( + string word, + Rectangle rectangle, + float fontSize, + Color? textColor = null, + string fontFamily = "Arial", + FontStyle fontStyle = FontStyle.Regular, + int frequency = 1, + double weight = 1.0) + { + Word = word ?? throw new ArgumentNullException(nameof(word)); + Rectangle = rectangle; + FontSize = fontSize; + TextColor = textColor; + FontFamily = fontFamily ?? throw new ArgumentNullException(nameof(fontFamily)); + FontStyle = fontStyle; + Frequency = frequency; + } + + public CloudItem WithRectangle(Rectangle newRectangle) + { + return new CloudItem(Word, newRectangle, FontSize, TextColor, FontFamily, FontStyle, Frequency); + } + + public CloudItem WithFontSize(float newFontSize) + { + return new CloudItem(Word, Rectangle, newFontSize, TextColor, FontFamily, FontStyle, Frequency); + } + + public CloudItem WithColor(Color newColor) + { + return new CloudItem(Word, Rectangle, FontSize, newColor, FontFamily, FontStyle, Frequency); + } + } +} diff --git a/TagCloudGenerator/Core/Models/TextSettings.cs b/TagCloudGenerator/Core/Models/TextSettings.cs new file mode 100644 index 00000000..a7ca6145 --- /dev/null +++ b/TagCloudGenerator/Core/Models/TextSettings.cs @@ -0,0 +1,43 @@ +using System.Drawing; + +namespace TagCloudGenerator.Core.Models +{ + public class TextSettings + { + public string FontFamily { get; private set; } = "Arial"; + public float MinFontSize { get; private set; } = 12f; + public float MaxFontSize { get; private set; } = 72f; + public Color TextColor { get; private set; } = Color.Black; + + public TextSettings SetFontFamily(string? font) + { + if (!string.IsNullOrWhiteSpace(font)) FontFamily = font; + return this; + } + + public TextSettings SetMinFontSize(float? size) + { + MinFontSize = size is >= 0 ? size.Value : MinFontSize; + return this; + } + + public TextSettings SetMaxFontSize(float? size) + { + MaxFontSize = size is >= 0 ? size.Value : MaxFontSize; + return this; + } + + public TextSettings SetFontSizeRange(float? minSize, float? maxSize) + { + SetMinFontSize(minSize); + SetMaxFontSize(maxSize); + return this; + } + + public TextSettings SetTextColor(Color? color) + { + if (color.HasValue) TextColor = color.Value; + return this; + } + } +} diff --git a/TagCloudGenerator/Core/Services/CloudGenerator.cs b/TagCloudGenerator/Core/Services/CloudGenerator.cs new file mode 100644 index 00000000..7bba0947 --- /dev/null +++ b/TagCloudGenerator/Core/Services/CloudGenerator.cs @@ -0,0 +1,96 @@ +using System.Drawing; +using TagCloudGenerator.Algorithms; +using TagCloudGenerator.Core.Interfaces; +using TagCloudGenerator.Core.Models; +using TagCloudGenerator.Infrastructure; +using TagCloudGenerator.Infrastructure.Filters; + +namespace TagCloudGenerator.Core.Services +{ + public class CloudGenerator : ITagCloudGenerator + { + private readonly ITagCloudAlgorithm _algorithm; + private readonly IAnalyzer _analyzer; + private readonly IRenderer _renderer; + private readonly IFontSizeCalculator _fontSizeCalculator; + private readonly ITextMeasurer _textMeasurer; + private readonly ISorterer _sorterer; + private readonly Point _center; + + public CloudGenerator(ITagCloudAlgorithm algorithm, + IAnalyzer analyzer, + IRenderer renderer, + IFontSizeCalculator fontSizeCalculator, + ITextMeasurer textMeasurer, + ISorterer sorterer) + { + this._algorithm = algorithm; + _analyzer = analyzer; + _renderer = renderer; + _fontSizeCalculator = fontSizeCalculator; + _textMeasurer = textMeasurer; + _sorterer = sorterer; + _center = new Point(0, 0); + } + + public Result Generate(List words, CanvasSettings canvasSettings, TextSettings textSettings, IEnumerable filters) + { + words = ApplyFilters(words, filters); + if(words.Count == 0) return Result.Fail("Found no words to render after filtering"); + + var wordsWithFreq = _sorterer.Sort(_analyzer.Analyze(words)); + + var initializedItems = InitializeCloudItems(wordsWithFreq, textSettings).ToList(); + + _algorithm.Reset(); + + return _renderer.Render(initializedItems, canvasSettings, textSettings); + } + + private List ApplyFilters(List words, IEnumerable filters) + { + var filteredWords = words; + foreach (var filter in filters) + { + filteredWords = filter.Filter(filteredWords); + } + return filteredWords; + } + + private IEnumerable InitializeCloudItems(IEnumerable<(string Word, int Frequency)> items, TextSettings settings) + { + var itemsList = new List(); + + var minFrequency = items.Min(i => i.Frequency); + var maxFrequency = items.Max(i => i.Frequency); + + foreach (var item in items) + { + var fontSize = _fontSizeCalculator.Calculate( + item.Frequency, + minFrequency, + maxFrequency, + settings.MinFontSize, + settings.MaxFontSize); + + var textSize = _textMeasurer.Measure( + item.Word, + fontSize, + settings.FontFamily); + + var itemRectangle = _algorithm.PutNextRectangle(textSize); + + itemsList.Add( + new CloudItem( + word: item.Word, + rectangle: itemRectangle, + fontSize: fontSize, + textColor: settings.TextColor, + fontFamily: settings.FontFamily, + frequency: item.Frequency + )); + } + return itemsList; + } + } +} diff --git a/TagCloudGenerator/DI/TagCloudModule.cs b/TagCloudGenerator/DI/TagCloudModule.cs new file mode 100644 index 00000000..3df3fe70 --- /dev/null +++ b/TagCloudGenerator/DI/TagCloudModule.cs @@ -0,0 +1,47 @@ +using Autofac; +using TagCloudGenerator.Algorithms; +using TagCloudGenerator.Core.Interfaces; +using TagCloudGenerator.Core.Services; +using TagCloudGenerator.Infrastructure.Analyzers; +using TagCloudGenerator.Infrastructure.Calculators; +using TagCloudGenerator.Infrastructure.Filters; +using TagCloudGenerator.Infrastructure.Measurers; +using TagCloudGenerator.Infrastructure.Normalizers; +using TagCloudGenerator.Infrastructure.Readers; +using TagCloudGenerator.Infrastructure.Renderers; +using TagCloudGenerator.Infrastructure.Sorterers; + +namespace TagCloudGenerator.DI +{ + public class TagCloudModule : Autofac.Module + { + protected override void Load(ContainerBuilder builder) + { + builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); + + builder.RegisterType() + .As() + .SingleInstance(); + + builder.RegisterType().As().SingleInstance(); + + builder.RegisterType().As(); + + builder.RegisterType().As(); + + builder.RegisterType() + .As(); + + builder.RegisterType() + .As(); + + builder.RegisterType() + .As(); + + builder.RegisterType().As(); + + builder.RegisterType().As(); + } + } +} diff --git a/TagCloudGenerator/Infrastructure/Analyzers/WordsFrequencyAnalyzer.cs b/TagCloudGenerator/Infrastructure/Analyzers/WordsFrequencyAnalyzer.cs new file mode 100644 index 00000000..f6308382 --- /dev/null +++ b/TagCloudGenerator/Infrastructure/Analyzers/WordsFrequencyAnalyzer.cs @@ -0,0 +1,26 @@ +using System.Drawing; +using TagCloudGenerator.Core.Interfaces; +using TagCloudGenerator.Core.Models; + +namespace TagCloudGenerator.Infrastructure.Analyzers +{ + public class WordsFrequencyAnalyzer : IAnalyzer + { + public Dictionary Analyze(List words) + { + var wordFreqDictionary = new Dictionary(); + + if (words == null || words.Count == 0) + return new Dictionary(); + + foreach (string word in words) + { + if(wordFreqDictionary.TryAdd(word, 1)) + continue; + else wordFreqDictionary[word]++; + } + + return wordFreqDictionary; + } + } +} diff --git a/TagCloudGenerator/Infrastructure/Calculators/LinearFontSizeCalculator.cs b/TagCloudGenerator/Infrastructure/Calculators/LinearFontSizeCalculator.cs new file mode 100644 index 00000000..8fc0ed54 --- /dev/null +++ b/TagCloudGenerator/Infrastructure/Calculators/LinearFontSizeCalculator.cs @@ -0,0 +1,16 @@ +using TagCloudGenerator.Core.Interfaces; + +namespace TagCloudGenerator.Infrastructure.Calculators +{ + public class LinearFontSizeCalculator : IFontSizeCalculator + { + public float Calculate(int wordFrequency, int minFrequency, int maxFrequency, float minFontSize, float maxFontSize) + { + if (minFrequency == maxFrequency) + return (minFontSize + maxFontSize) / 2; + + float frequencyRatio = (float)(wordFrequency - minFrequency) / (maxFrequency - minFrequency); + return minFontSize + frequencyRatio * (maxFontSize - minFontSize); + } + } +} diff --git a/TagCloudGenerator/Infrastructure/Filters/BoringWordsFilter.cs b/TagCloudGenerator/Infrastructure/Filters/BoringWordsFilter.cs new file mode 100644 index 00000000..4cfdbd03 --- /dev/null +++ b/TagCloudGenerator/Infrastructure/Filters/BoringWordsFilter.cs @@ -0,0 +1,28 @@ +using TagCloudGenerator.Core.Interfaces; + +namespace TagCloudGenerator.Infrastructure.Filters +{ + public class BoringWordsFilter : IFilter + { + private string[] boringWords = ["in", "it", "a", "as", "for", "of", "on"]; + + public BoringWordsFilter() { } + + public BoringWordsFilter(IEnumerable words) + { + boringWords = words.ToArray(); + } + + public List Filter(List words) + { + if (words == null || words.Count == 0) return new List(); + + return words.Where(w => !boringWords.Contains(w)).ToList(); + } + + public bool ShouldInclude(string word) + { + return !boringWords.Contains(word); + } + } +} diff --git a/TagCloudGenerator/Infrastructure/Measurers/GraphicsTextMeasurer.cs b/TagCloudGenerator/Infrastructure/Measurers/GraphicsTextMeasurer.cs new file mode 100644 index 00000000..5ecdd40c --- /dev/null +++ b/TagCloudGenerator/Infrastructure/Measurers/GraphicsTextMeasurer.cs @@ -0,0 +1,18 @@ +using System.Drawing; +using TagCloudGenerator.Core.Interfaces; + +namespace TagCloudGenerator.Infrastructure.Measurers +{ + public class GraphicsTextMeasurer : ITextMeasurer + { + public Size Measure(string word, float fontSize, string fontFamily) + { + using var font = new Font(fontFamily, fontSize); + using var bitmap = new Bitmap(1, 1); + using var graphics = Graphics.FromImage(bitmap); + + var size = graphics.MeasureString(word, font); + return new Size((int)Math.Ceiling(size.Width), (int)Math.Ceiling(size.Height)); + } + } +} diff --git a/TagCloudGenerator/Infrastructure/Normalizers/LowerCaseNormalizer.cs b/TagCloudGenerator/Infrastructure/Normalizers/LowerCaseNormalizer.cs new file mode 100644 index 00000000..83ef709b --- /dev/null +++ b/TagCloudGenerator/Infrastructure/Normalizers/LowerCaseNormalizer.cs @@ -0,0 +1,14 @@ +using TagCloudGenerator.Core.Interfaces; + +namespace TagCloudGenerator.Infrastructure.Normalizers +{ + public class LowerCaseNormalizer : INormalizer + { + public List Normalize(List words) + { + if (words == null || words.Count == 0) return new List(); + + return words.Select(w => w.ToLower()).ToList(); + } + } +} diff --git a/TagCloudGenerator/Infrastructure/Readers/DocxReader.cs b/TagCloudGenerator/Infrastructure/Readers/DocxReader.cs new file mode 100644 index 00000000..6df98a7b --- /dev/null +++ b/TagCloudGenerator/Infrastructure/Readers/DocxReader.cs @@ -0,0 +1,28 @@ +using DocumentFormat.OpenXml.Packaging; +using DocumentFormat.OpenXml.Wordprocessing; +using TagCloudGenerator.Core.Interfaces; + +namespace TagCloudGenerator.Infrastructure.Readers +{ + public class DocxReader : IFormatReader + { + public bool CanRead(string filePath) + { + return Path.GetExtension(filePath).Equals(".docx", StringComparison.OrdinalIgnoreCase); + } + + public Result> TryRead(string filePath) + { + return Result.Of(() => + { + using var doc = WordprocessingDocument.Open(filePath, false); + + return doc.MainDocumentPart.Document.Body + .Elements() + .Select(p => p.InnerText) + .Where(t => !string.IsNullOrWhiteSpace(t)) + .ToList(); + }, "Failed to read DOCX file"); + } + } +} diff --git a/TagCloudGenerator/Infrastructure/Readers/ReaderRepository.cs b/TagCloudGenerator/Infrastructure/Readers/ReaderRepository.cs new file mode 100644 index 00000000..9110764e --- /dev/null +++ b/TagCloudGenerator/Infrastructure/Readers/ReaderRepository.cs @@ -0,0 +1,33 @@ +using TagCloudGenerator.Core.Interfaces; + +namespace TagCloudGenerator.Infrastructure.Readers +{ + public class ReaderRepository : IReaderRepository + { + private readonly IEnumerable _readers; + + public ReaderRepository(IEnumerable readers) + { + _readers = readers; + } + + public bool CanRead(string filePath) + { + return _readers.Any(r => r.CanRead(filePath)); + } + + public Result TryGetReader(string filePath) + { + if (string.IsNullOrWhiteSpace(filePath)) + return Result.Fail("File path is empty"); + + var reader = _readers.FirstOrDefault(r => r.CanRead(filePath)); + + if (reader == null) + return Result.Fail( + $"No reader found for file '{filePath}'"); + + return Result.Ok(reader); + } + } +} \ No newline at end of file diff --git a/TagCloudGenerator/Infrastructure/Readers/TxtReader.cs b/TagCloudGenerator/Infrastructure/Readers/TxtReader.cs new file mode 100644 index 00000000..1d5a5461 --- /dev/null +++ b/TagCloudGenerator/Infrastructure/Readers/TxtReader.cs @@ -0,0 +1,17 @@ +using TagCloudGenerator.Core.Interfaces; + +namespace TagCloudGenerator.Infrastructure.Readers +{ + public class TxtReader : IFormatReader + { + public bool CanRead(string filePath) + { + return Path.GetExtension(filePath).Equals(".txt", StringComparison.OrdinalIgnoreCase); + } + + public Result> TryRead(string filePath) + { + return Result.Of(() => { return File.ReadAllLines(filePath).ToList(); }, "Faield to read .txt file"); + } + } +} diff --git a/TagCloudGenerator/Infrastructure/Renderers/PngRenderer.cs b/TagCloudGenerator/Infrastructure/Renderers/PngRenderer.cs new file mode 100644 index 00000000..124b9c95 --- /dev/null +++ b/TagCloudGenerator/Infrastructure/Renderers/PngRenderer.cs @@ -0,0 +1,126 @@ +using System.Drawing; +using System.Drawing.Imaging; +using TagCloudGenerator.Core.Interfaces; +using TagCloudGenerator.Core.Models; + +namespace TagCloudGenerator.Infrastructure.Renderers +{ + public class PngRenderer : IRenderer + { + public Result Render(IEnumerable items, CanvasSettings canvasSettings, TextSettings textSettings) + { + if (items == null) + return Result.Fail("Cloud items are null"); + + var itemsList = items.ToList(); + + if (itemsList.Count == 0) + return Result.Fail("Cloud items are empty"); + + if (canvasSettings.CanvasSize.Width <= 0 || + canvasSettings.CanvasSize.Height <= 0) + return Result.Fail("Invalid canvas size"); + + var cloudBounds = CalculateCloudBounds(itemsList); + + if (!FitsCanvas(cloudBounds, canvasSettings.CanvasSize)) + return Result.Fail( + $"Tag cloud size ({cloudBounds.Width}x{cloudBounds.Height}) " + + $"exceeds canvas size ({canvasSettings.CanvasSize.Width}x{canvasSettings.CanvasSize.Height})"); + + return Result.Of(() => + { + var bitmap = new Bitmap(canvasSettings.CanvasSize.Width, canvasSettings.CanvasSize.Height); + using var graphics = Graphics.FromImage(bitmap); + + ConfigureGraphics(graphics); + graphics.Clear(canvasSettings.BackgroundColor); + + var (offsetX, offsetY) = CalculateOffset(itemsList, canvasSettings); + + using var brush = new SolidBrush(textSettings.TextColor); + using var stringFormat = new StringFormat { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center }; + using var pen = new Pen(textSettings.TextColor, 1) { DashStyle = System.Drawing.Drawing2D.DashStyle.Dash }; + + foreach (var item in itemsList) + DrawCloudItem(graphics, item, offsetX, offsetY, canvasSettings, textSettings, brush, pen, stringFormat); + return bitmap; + }, "Failed to render tag cloud image"); + } + + private (int offsetX, int offsetY) CalculateOffset( + List items, + CanvasSettings settings) + { + var minX = items.Min(i => i.Rectangle.X); + var minY = items.Min(i => i.Rectangle.Y); + var maxX = items.Max(i => i.Rectangle.Right); + var maxY = items.Max(i => i.Rectangle.Bottom); + + var cloudWidth = maxX - minX; + var cloudHeight = maxY - minY; + + var offsetX = (settings.CanvasSize.Width - cloudWidth) / 2 - minX; + var offsetY = (settings.CanvasSize.Height - cloudHeight) / 2 - minY; + + return (offsetX, offsetY); + } + + private void DrawCloudItem( + Graphics graphics, + CloudItem item, + int offsetX, + int offsetY, + CanvasSettings canvasSettings, + TextSettings textSettings, + SolidBrush brush, + Pen pen, + StringFormat stringFormat) + { + var drawRect = new Rectangle( + item.Rectangle.X + offsetX, + item.Rectangle.Y + offsetY, + item.Rectangle.Width, + item.Rectangle.Height); + + var color = item.TextColor ?? textSettings.TextColor; + + using var font = new Font( + item.FontFamily, + item.FontSize, + item.FontStyle, + GraphicsUnit.Pixel); + + graphics.DrawString(item.Word, font, brush, drawRect, stringFormat); + + if (canvasSettings.ShowRectangles) + { + graphics.DrawRectangle(pen, drawRect); + } + } + + private void ConfigureGraphics(Graphics graphics) + { + graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias; + graphics.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAliasGridFit; + graphics.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic; + graphics.CompositingQuality = System.Drawing.Drawing2D.CompositingQuality.HighQuality; + } + + private Rectangle CalculateCloudBounds(List items) + { + var minX = items.Min(i => i.Rectangle.Left); + var minY = items.Min(i => i.Rectangle.Top); + var maxX = items.Max(i => i.Rectangle.Right); + var maxY = items.Max(i => i.Rectangle.Bottom); + + return Rectangle.FromLTRB(minX, minY, maxX, maxY); + } + + private bool FitsCanvas(Rectangle cloudBounds, Size canvasSize) + { + return cloudBounds.Width <= canvasSize.Width + && cloudBounds.Height <= canvasSize.Height; + } + } +} diff --git a/TagCloudGenerator/Infrastructure/Result.cs b/TagCloudGenerator/Infrastructure/Result.cs new file mode 100644 index 00000000..39ca803c --- /dev/null +++ b/TagCloudGenerator/Infrastructure/Result.cs @@ -0,0 +1,140 @@ +namespace TagCloudGenerator.Infrastructure +{ + public class None + { + private None() + { + } + } + + public struct Result + { + public Result(string error, T value = default) + { + Error = error; + Value = value; + } + public static implicit operator Result(T v) + { + return Result.Ok(v); + } + + public string Error { get; } + internal T Value { get; } + public T GetValueOrThrow() + { + if (IsSuccess) return Value; + throw new InvalidOperationException($"No value. Only Error {Error}"); + } + public bool IsSuccess => Error == null; + } + + public static class Result + { + public static Result AsResult(this T value) + { + return Ok(value); + } + + public static Result Ok(T value) + { + return new Result(null, value); + } + public static Result Ok() + { + return Ok(null); + } + + public static Result Fail(string e) + { + return new Result(e); + } + + public static Result Of(Func f, string error = null) + { + try + { + return Ok(f()); + } + catch (Exception e) + { + return Fail(error ?? e.Message); + } + } + + public static Result OfAction(Action f, string error = null) + { + try + { + f(); + return Ok(); + } + catch (Exception e) + { + return Fail(error ?? e.Message); + } + } + + public static Result Then( + this Result input, + Func continuation) + { + return input.Then(inp => Of(() => continuation(inp))); + } + + public static Result Then( + this Result input, + Action continuation) + { + return input.Then(inp => OfAction(() => continuation(inp))); + } + + public static Result Then( + this Result input, + Action continuation) + { + return input.Then(inp => OfAction(() => continuation(inp))); + } + + public static Result Then( + this Result input, + Func> continuation) + { + return input.IsSuccess + ? continuation(input.Value) + : Fail(input.Error); + } + + public static Result OnFail( + this Result input, + Action handleError) + { + if (!input.IsSuccess) handleError(input.Error); + return input; + } + + public static Result OnSuccess( + this Result result, + Action action) + { + if (result.IsSuccess) + action(result.Value); + return result; + } + + public static Result ReplaceError( + this Result input, + Func replaceError) + { + if (input.IsSuccess) return input; + return Fail(replaceError(input.Error)); + } + + public static Result RefineError( + this Result input, + string errorMessage) + { + return input.ReplaceError(err => errorMessage + ". " + err); + } + } +} diff --git a/TagCloudGenerator/Infrastructure/Sorterers/FrequencyDescendingSorterer.cs b/TagCloudGenerator/Infrastructure/Sorterers/FrequencyDescendingSorterer.cs new file mode 100644 index 00000000..0bdc20c3 --- /dev/null +++ b/TagCloudGenerator/Infrastructure/Sorterers/FrequencyDescendingSorterer.cs @@ -0,0 +1,16 @@ +using TagCloudGenerator.Core.Interfaces; + +namespace TagCloudGenerator.Infrastructure.Sorterers +{ + public class FrequencyDescendingSorterer : ISorterer + { + public List<(string Word, int Frequency)> Sort(Dictionary wordsWithFreqs) + { + return wordsWithFreqs + .OrderByDescending(w => w.Value) + .ThenBy(w => w.Key) + .Select(kvpair => (kvpair.Key, kvpair.Value)) + .ToList(); + } + } +} diff --git a/TagCloudGenerator/TagCloudGenerator.csproj b/TagCloudGenerator/TagCloudGenerator.csproj new file mode 100644 index 00000000..3937f9c9 --- /dev/null +++ b/TagCloudGenerator/TagCloudGenerator.csproj @@ -0,0 +1,17 @@ + + + + Library + net8.0-windows + enable + disable + + + + + + + + + + diff --git a/TagCloudGeneratorTests/BoringWordsFilterTests.cs b/TagCloudGeneratorTests/BoringWordsFilterTests.cs new file mode 100644 index 00000000..52232f6a --- /dev/null +++ b/TagCloudGeneratorTests/BoringWordsFilterTests.cs @@ -0,0 +1,82 @@ +using NUnit.Framework; +using TagCloudGenerator.Infrastructure.Filters; + +namespace TagCloudGeneratorTests +{ + public class BoringWordsFilterTests + { + private BoringWordsFilter filter = new BoringWordsFilter(); + + [Test] + public void Filter_EmptyInput_ReturnsEmpty() + { + var words = new List(); + var result = filter.Filter(words); + Assert.That(result, Is.Empty); + } + + [Test] + public void Filter_RemovesBoringWords_Test() + { + var words = new List { "hello", "in", "world", "a", "test" }; + var result = filter.Filter(words).ToList(); + + Assert.That(result.Count, Is.EqualTo(3)); + Assert.That(result, Contains.Item("hello")); + Assert.That(result, Contains.Item("world")); + Assert.That(result, Contains.Item("test")); + Assert.That(result, Does.Not.Contain("in")); + Assert.That(result, Does.Not.Contain("a")); + } + + [Test] + public void Filter_AllBoringWords_ReturnsEmpty_Test() + { + var words = new List { "in", "a", "for", "on" }; + var result = filter.Filter(words); + Assert.That(result, Is.Empty); + } + + [Test] + public void Filter_NoBoringWords_ReturnsAllWords_Test() + { + var words = new List { "hello", "world", "test" }; + var result = filter.Filter(words).ToList(); + + Assert.That(result.Count, Is.EqualTo(3)); + Assert.That(result, Is.EquivalentTo(words)); + } + + [Test] + public void ShouldInclude_ReturnsFalseForBoringWords_Test() + { + var boringWords = new List { "in", "it", "a", "as", "for", "of", "on" }; + foreach (var word in boringWords) + { + Assert.That(filter.ShouldInclude(word), Is.False); + } + } + + [Test] + public void ShouldInclude_ReturnsTrueForNormalWords_Test() + { + var normalWords = new List { "hello", "world", "computer", "programming", "test" }; + foreach (var word in normalWords) + { + Assert.That(filter.ShouldInclude(word), Is.True); + } + } + + [Test] + public void Filter_PreservesOrder_Test() + { + var words = new List { "hello", "in", "world", "a", "test", "for" }; + var result = filter.Filter(words).ToList(); + + Assert.That(result.Count, Is.EqualTo(3)); + Assert.That(result[0], Is.EqualTo("hello")); + Assert.That(result[1], Is.EqualTo("world")); + Assert.That(result[2], Is.EqualTo("test")); + } + } +} diff --git a/TagCloudGeneratorTests/CloudGeneratorTests.cs b/TagCloudGeneratorTests/CloudGeneratorTests.cs new file mode 100644 index 00000000..402bab9d --- /dev/null +++ b/TagCloudGeneratorTests/CloudGeneratorTests.cs @@ -0,0 +1,165 @@ +using Moq; +using NUnit.Framework; +using System.Drawing; +using TagCloudGenerator.Core.Interfaces; +using TagCloudGenerator.Core.Models; +using TagCloudGenerator.Core.Services; + +namespace TagCloudGeneratorTests +{ + public class CloudGeneratorTests + { + private Mock algorithmMock; + private Mock filterMock; + private Mock analyzerMock; + private Mock rendererMock; + private Mock fontSizeCalculatorMock; + private Mock textMeasurerMock; + private Mock sortererMock; + private CloudGenerator cloudGenerator; + + [SetUp] + public void Setup() + { + algorithmMock = new Mock(); + filterMock = new Mock(); + analyzerMock = new Mock(); + rendererMock = new Mock(); + fontSizeCalculatorMock = new Mock(); + textMeasurerMock = new Mock(); + sortererMock = new Mock(); + + cloudGenerator = new CloudGenerator( + algorithmMock.Object, + analyzerMock.Object, + rendererMock.Object, + fontSizeCalculatorMock.Object, + textMeasurerMock.Object, + sortererMock.Object); + } + + [Test] + public void Generate_EmptyWords_ReturnsFailResult_AndDoesNotRender_Test() + { + filterMock + .Setup(f => f.Filter(It.IsAny>())) + .Returns(new List()); + + var result = cloudGenerator.Generate( + new List(), + new CanvasSettings(), + new TextSettings(), + new[] { filterMock.Object }); + + Assert.That(result.IsSuccess, Is.False); + + rendererMock.Verify(r => r.Render( + It.IsAny>(), + It.IsAny(), + It.IsAny()), + Times.Never); + } + + [Test] + public void Generate_AllWordsFilteredOut_ReturnsFailResult_Test() + { + var words = new List { "in", "a", "for" }; + + filterMock + .Setup(f => f.Filter(words)) + .Returns(new List()); + + var result = cloudGenerator.Generate( + words, + new CanvasSettings(), + new TextSettings(), + new[] { filterMock.Object }); + + Assert.That(result.IsSuccess, Is.False); + + rendererMock.Verify(r => r.Render( + It.IsAny>(), + It.IsAny(), + It.IsAny()), + Times.Never); + } + + [Test] + public void Generate_NormalFlow_CallsAllDependencies_Test() + { + var words = new List { "hello", "world", "hello" }; + var filteredWords = new List { "hello", "world", "hello" }; + + var analyzed = new Dictionary { { "hello", 2 }, { "world", 1 } }; + + var sorted = new List<(string Word, int Frequency)> { ("hello", 2), ("world", 1) }; + + filterMock + .Setup(f => f.Filter(words)) + .Returns(filteredWords); + + analyzerMock + .Setup(a => a.Analyze(filteredWords)) + .Returns(analyzed); + + sortererMock + .Setup(s => s.Sort(analyzed)) + .Returns(sorted); + + var textSettings = new TextSettings() + .SetFontFamily("Arial") + .SetFontSizeRange(12, 72); + + fontSizeCalculatorMock + .Setup(f => f.Calculate(2, 1, 2, 12f, 72f)) + .Returns(50f); + + fontSizeCalculatorMock + .Setup(f => f.Calculate(1, 1, 2, 12f, 72f)) + .Returns(20f); + + textMeasurerMock + .Setup(t => t.Measure("hello", 50f, "Arial")) + .Returns(new Size(100, 30)); + + textMeasurerMock + .Setup(t => t.Measure("world", 20f, "Arial")) + .Returns(new Size(80, 25)); + + algorithmMock + .Setup(a => a.PutNextRectangle(It.IsAny())) + .Returns(new Rectangle(0, 0, 100, 30)); + + rendererMock + .Setup(r => r.Render( + It.IsAny>(), + It.IsAny(), + It.IsAny())) + .Returns(new Bitmap(1, 1)); + + var result = cloudGenerator.Generate( + words, + new CanvasSettings(), + textSettings, + new[] { filterMock.Object }); + + Assert.That(result.IsSuccess, Is.True); + result.GetValueOrThrow().Dispose(); + + filterMock.Verify(f => f.Filter(words), Times.Once); + analyzerMock.Verify(a => a.Analyze(filteredWords), Times.Once); + algorithmMock.Verify(a => a.Reset(), Times.Once); + algorithmMock.Verify(a => a.PutNextRectangle(It.IsAny()), Times.Exactly(2)); + + rendererMock.Verify(r => r.Render( + It.Is>(items => items.Count() == 2), + It.IsAny(), + It.Is(ts => + ts.FontFamily == "Arial" && + Math.Abs(ts.MinFontSize - 12) < float.Epsilon && + Math.Abs(ts.MaxFontSize - 72) < float.Epsilon)), + Times.Once); + } + + } +} diff --git a/TagCloudGeneratorTests/DocxReaderTests.cs b/TagCloudGeneratorTests/DocxReaderTests.cs new file mode 100644 index 00000000..0198c87a --- /dev/null +++ b/TagCloudGeneratorTests/DocxReaderTests.cs @@ -0,0 +1,97 @@ +using DocumentFormat.OpenXml.Packaging; +using DocumentFormat.OpenXml.Wordprocessing; +using NUnit.Framework; +using TagCloudGenerator.Infrastructure; +using TagCloudGenerator.Infrastructure.Readers; + +namespace TagCloudGeneratorTests +{ + public class DocxReaderTests + { + private string tempFilePath; + private DocxReader reader; + + [SetUp] + public void Setup() + { + tempFilePath = Path.Combine( + Path.GetTempPath(), + Guid.NewGuid() + ".docx"); + + reader = new DocxReader(); + } + + [TearDown] + public void TearDown() + { + if (File.Exists(tempFilePath)) + File.Delete(tempFilePath); + } + + [Test] + public void CanRead_DocxExtension_ReturnsTrue_Test() + { + Assert.That(reader.CanRead("file.docx"), Is.True); + Assert.That(reader.CanRead("file.txt"), Is.False); + } + + [Test] + public void TryRead_DocxWithParagraphs_ReturnsParagraphs_Test() + { + CreateDocx(new[] { "word1", "word2", "word3" }); + + var result = reader.TryRead(tempFilePath); + + Assert.That(result.IsSuccess, Is.True); + Assert.That(result.GetValueOrThrow(), Is.EqualTo(new[] { "word1", "word2", "word3" })); + } + + [Test] + public void TryRead_EmptyDocx_ReturnsEmptyList_Test() + { + CreateDocx(Array.Empty()); + + var result = reader.TryRead(tempFilePath); + + Assert.That(result.IsSuccess, Is.True); + Assert.That(result.GetValueOrThrow(), Is.Empty); + } + + [Test] + public void TryRead_DocxWithEmptyParagraphs_IgnoresThem_Test() + { + CreateDocx(new[] { "word1", "", " ", "word2" }); + var result = reader.TryRead(tempFilePath); + + Assert.That(result.IsSuccess, Is.True); + Assert.That(result.GetValueOrThrow(), Is.EqualTo(new[] { "word1", "word2" })); + } + + [Test] + public void TryRead_NonExistentFile_ReturnsFailResult_Test() + { + var result = reader.TryRead("nonexistent.docx"); + + Assert.That(result.IsSuccess, Is.False); + } + + private void CreateDocx(IEnumerable lines) + { + using var doc = WordprocessingDocument.Create( + tempFilePath, + DocumentFormat.OpenXml.WordprocessingDocumentType.Document); + + var body = new Body(); + + foreach (var line in lines) + { + body.AppendChild( + new Paragraph( + new Run(new Text(line)))); + } + + doc.AddMainDocumentPart().Document = + new Document(body); + } + } +} diff --git a/TagCloudGeneratorTests/GraphicsTextMeasurerTests.cs b/TagCloudGeneratorTests/GraphicsTextMeasurerTests.cs new file mode 100644 index 00000000..ecfa6b63 --- /dev/null +++ b/TagCloudGeneratorTests/GraphicsTextMeasurerTests.cs @@ -0,0 +1,77 @@ +using NUnit.Framework; +using TagCloudGenerator.Infrastructure.Measurers; + +namespace TagCloudGeneratorTests +{ + public class GraphicsTextMeasurerTests + { + private GraphicsTextMeasurer measurer = new GraphicsTextMeasurer(); + + [Test] + public void Measure_EmptyString_ReturnsValidSize_Test() + { + var result = measurer.Measure("", 12f, "Arial"); + Assert.That(result.Width, Is.GreaterThanOrEqualTo(0)); + Assert.That(result.Height, Is.GreaterThanOrEqualTo(0)); + } + + [Test] + public void Measure_SingleCharacter_ReturnsNonZeroSize_Test() + { + var result = measurer.Measure("A", 12f, "Arial"); + Assert.That(result.Width, Is.GreaterThan(0)); + Assert.That(result.Height, Is.GreaterThan(0)); + } + + [Test] + public void Measure_LongerWord_ReturnsWiderSize_Test() + { + var shortSize = measurer.Measure("A", 12f, "Arial"); + var longSize = measurer.Measure("ABCDEFGHIJ", 12f, "Arial"); + Assert.That(longSize.Width, Is.GreaterThan(shortSize.Width)); + } + + [Test] + public void Measure_LargerFont_ReturnsLargerSize_Test() + { + var smallSize = measurer.Measure("Test", 12f, "Arial"); + var largeSize = measurer.Measure("Test", 24f, "Arial"); + Assert.That(largeSize.Width, Is.GreaterThan(smallSize.Width)); + Assert.That(largeSize.Height, Is.GreaterThan(smallSize.Height)); + } + + [Test] + public void Measure_DifferentFontFamily_ReturnsDifferentSize_Test() + { + var arialSize = measurer.Measure("Test", 12f, "Arial"); + var timesSize = measurer.Measure("Test", 12f, "Times New Roman"); + Assert.That(timesSize.Width, Is.GreaterThan(0)); + Assert.That(timesSize.Height, Is.GreaterThan(0)); + } + + [Test] + public void Measure_SameParametersTwice_ReturnsSameSize_Test() + { + var size1 = measurer.Measure("Consistent", 16f, "Arial"); + var size2 = measurer.Measure("Consistent", 16f, "Arial"); + Assert.That(size1.Width, Is.EqualTo(size2.Width)); + Assert.That(size1.Height, Is.EqualTo(size2.Height)); + } + + [Test] + public void Measure_WithSpaces_ReturnsCorrectSize_Test() + { + var sizeWithoutSpace = measurer.Measure("HelloWorld", 12f, "Arial"); + var sizeWithSpace = measurer.Measure("Hello World", 12f, "Arial"); + Assert.That(sizeWithSpace.Width, Is.GreaterThan(sizeWithoutSpace.Width)); + } + + [Test] + public void Measure_VeryLargeFont_ReturnsProportionalSize_Test() + { + var result = measurer.Measure("Test", 100f, "Arial"); + Assert.That(result.Width, Is.GreaterThan(50)); + Assert.That(result.Height, Is.GreaterThan(50)); + } + } +} diff --git a/TagCloudGeneratorTests/LinearFontSizeCalculatorTests.cs b/TagCloudGeneratorTests/LinearFontSizeCalculatorTests.cs new file mode 100644 index 00000000..b9fe20df --- /dev/null +++ b/TagCloudGeneratorTests/LinearFontSizeCalculatorTests.cs @@ -0,0 +1,71 @@ +using NUnit.Framework; +using TagCloudGenerator.Infrastructure.Calculators; + +namespace TagCloudGeneratorTests +{ + public class LinearFontSizeCalculatorTests + { + private LinearFontSizeCalculator calculator = new LinearFontSizeCalculator(); + + [Test] + public void Calculate_MinFrequency_ReturnsMinFontSize_Test() + { + var result = calculator.Calculate(1, 1, 10, 12f, 72f); + Assert.That(result, Is.EqualTo(12f)); + } + + [Test] + public void Calculate_MaxFrequency_ReturnsMaxFontSize_Test() + { + var result = calculator.Calculate(10, 1, 10, 12f, 72f); + Assert.That(result, Is.EqualTo(72f)); + } + + [Test] + public void Calculate_MiddleFrequency_ReturnsProportionalSize_Test() + { + var result = calculator.Calculate(5, 1, 9, 10f, 50f); + Assert.That(result, Is.EqualTo(30f).Within(0.001f)); + } + + [Test] + public void Calculate_SameMinMaxFrequency_ReturnsAverage_Test() + { + var result = calculator.Calculate(5, 5, 5, 12f, 72f); + Assert.That(result, Is.EqualTo(42f)); + } + + [Test] + public void Calculate_FrequencyBelowMin_ReturnsLessThanMinFontSize_Test() + { + var result = calculator.Calculate(0, 1, 10, 12f, 72f); + Assert.That(result, Is.EqualTo(5.333f).Within(0.001f)); + } + + [Test] + public void Calculate_FrequencyAboveMax_ReturnsMoreThanMaxFontSize_Test() + { + var result = calculator.Calculate(15, 1, 10, 12f, 72f); + Assert.That(result, Is.EqualTo(105.333f).Within(0.001f)); + } + + [Test] + public void Calculate_ZeroRangeFontSizes_ReturnsMinFontSize_Test() + { + var result = calculator.Calculate(5, 1, 10, 20f, 20f); + Assert.That(result, Is.EqualTo(20f)); + } + + [Test] + [TestCase(1, 10f)] + [TestCase(3, 20f)] + [TestCase(5, 30f)] + [TestCase(7, 40f)] + [TestCase(9, 50f)] + public void Calculate_MultipleTestCases_ReturnsCorrectValues_Test(int frequency, float expected) + { + var result = calculator.Calculate(frequency, 1, 9, 10f, 50f); + Assert.That(result, Is.EqualTo(expected).Within(0.001f)); + } + } +} diff --git a/TagCloudGeneratorTests/ReaderRepositoryTests.cs b/TagCloudGeneratorTests/ReaderRepositoryTests.cs new file mode 100644 index 00000000..e9c7f643 --- /dev/null +++ b/TagCloudGeneratorTests/ReaderRepositoryTests.cs @@ -0,0 +1,73 @@ +using Moq; +using NUnit.Framework; +using TagCloudGenerator.Core.Interfaces; +using TagCloudGenerator.Infrastructure.Readers; + +namespace TagCloudGeneratorTests +{ + public class ReaderRepositoryTests + { + private Mock firstReaderMock; + private Mock secondReaderMock; + private ReaderRepository readerRepository; + + [SetUp] + public void Setup() + { + firstReaderMock = new Mock(); + secondReaderMock = new Mock(); + + readerRepository = new ReaderRepository( + new[] { firstReaderMock.Object, secondReaderMock.Object }); + } + + [Test] + public void CanRead_WhenAnyReaderCanRead_ReturnsTrue() + { + firstReaderMock.Setup(r => r.CanRead("file.docx")).Returns(false); + secondReaderMock.Setup(r => r.CanRead("file.docx")).Returns(true); + + var result = readerRepository.CanRead("file.docx"); + + Assert.That(result, Is.True); + } + + [Test] + public void CanRead_WhenNoReaderCanRead_ReturnsFalse() + { + firstReaderMock.Setup(r => r.CanRead(It.IsAny())).Returns(false); + secondReaderMock.Setup(r => r.CanRead(It.IsAny())).Returns(false); + + var result = readerRepository.CanRead("file.unknown"); + + Assert.That(result, Is.False); + } + + [Test] + public void TryRead_UsesFirstMatchingReader() + { + var expected = new List { "word1", "word2" }; + + firstReaderMock.Setup(r => r.CanRead("file.docx")).Returns(false); + secondReaderMock.Setup(r => r.CanRead("file.docx")).Returns(true); + secondReaderMock.Setup(r => r.TryRead("file.docx")).Returns(expected); + + var result = readerRepository.TryGetReader("file.docx"); + Assert.That(result.IsSuccess, Is.True); + + Assert.That(result.GetValueOrThrow().TryRead("file.docx").GetValueOrThrow(), Is.EqualTo(expected)); + + secondReaderMock.Verify(r => r.TryRead("file.docx"), Times.Once); + firstReaderMock.Verify(r => r.TryRead(It.IsAny()), Times.Never); + } + + [Test] + public void TryRead_WhenNoReaderFound_ReturnsError() + { + firstReaderMock.Setup(r => r.CanRead(It.IsAny())).Returns(false); + secondReaderMock.Setup(r => r.CanRead(It.IsAny())).Returns(false); + + Assert.That(readerRepository.TryGetReader("file.xyz").IsSuccess, Is.False); + } + } +} diff --git a/TagCloudGeneratorTests/TagCloudGeneratorTests.csproj b/TagCloudGeneratorTests/TagCloudGeneratorTests.csproj new file mode 100644 index 00000000..94cb41bc --- /dev/null +++ b/TagCloudGeneratorTests/TagCloudGeneratorTests.csproj @@ -0,0 +1,25 @@ + + + + net8.0-windows + enable + enable + + false + true + + + + + + + + + + + + + + + + diff --git a/TagCloudGeneratorTests/TxtReaderTests.cs b/TagCloudGeneratorTests/TxtReaderTests.cs new file mode 100644 index 00000000..d9d8825f --- /dev/null +++ b/TagCloudGeneratorTests/TxtReaderTests.cs @@ -0,0 +1,103 @@ +using NUnit.Framework; +using TagCloudGenerator.Infrastructure.Readers; + +namespace TagCloudGeneratorTests +{ + public class TxtReaderTests + { + private TxtReader reader = new TxtReader(); + private string tempFilePath = Path.GetTempFileName(); + + [TearDown] + public void TearDown() + { + if (File.Exists(tempFilePath)) + File.Delete(tempFilePath); + } + + [Test] + public void TryRead_ExistingFile_ReturnsLines_Test() + { + var expectedLines = new[] { "line1", "line2", "line3" }; + WriteToTempFile(string.Join(Environment.NewLine, expectedLines)); + + var result = reader.TryRead(tempFilePath); + Assert.That(result.IsSuccess, Is.True); + Assert.That(result.GetValueOrThrow().ToList(), Is.EqualTo(expectedLines)); + } + + [Test] + public void TryRead_EmptyFile_ReturnsEmpty_Test() + { + WriteToTempFile(""); + var result = reader.TryRead(tempFilePath); + Assert.That(result.IsSuccess, Is.True); + Assert.That(result.GetValueOrThrow(), Is.Empty); + } + + [Test] + public void TryRead_FileWithWhitespaceLines_ReturnsAllLines_Test() + { + WriteToTempFile("line1\n\nline2\n \nline3"); + var result = reader.TryRead(tempFilePath); + + Assert.That(result.IsSuccess, Is.True); + var lines = result.GetValueOrThrow().ToList(); + + Assert.That(lines.Count, Is.EqualTo(5)); + Assert.That(lines[0], Is.EqualTo("line1")); + Assert.That(lines[1], Is.EqualTo("")); + Assert.That(lines[2], Is.EqualTo("line2")); + Assert.That(lines[3], Is.EqualTo(" ")); + Assert.That(lines[4], Is.EqualTo("line3")); + } + + [Test] + public void TryRead_FileWithWindowsLineEndings_ReturnsCorrectLines_Test() + { + WriteToTempFile("line1\r\nline2\r\nline3"); + var result = reader.TryRead(tempFilePath); + + Assert.That(result.IsSuccess, Is.True); + Assert.That(result.GetValueOrThrow().ToList(), Is.EqualTo(new[] { "line1", "line2", "line3" })); + } + + [Test] + public void TryRead_NonExistentFile_ReturnsNull_Test() + { + var nonExistentPath = "C:\\nonexistent\\file.txt"; + var result = reader.TryRead(nonExistentPath); + Assert.That(result.IsSuccess, Is.False); + } + + [Test] + public void TryRead_FileWithOneLine_ReturnsOneLine_Test() + { + WriteToTempFile("single line"); + var result = reader.TryRead(tempFilePath); + + Assert.That(result.IsSuccess, Is.True); + Assert.That(result.GetValueOrThrow().Count, Is.EqualTo(1)); + Assert.That(result.GetValueOrThrow()[0], Is.EqualTo("single line")); + } + + [Test] + public void TryRead_FileWithTrailingNewline_ReturnsCorrectLines_Test() + { + WriteToTempFile("line1\nline2\n"); + var result = reader.TryRead(tempFilePath); + + Assert.That(result.IsSuccess, Is.True); + + var lines = result.GetValueOrThrow(); + Assert.That(lines.Count, Is.EqualTo(2)); + Assert.That(lines[0], Is.EqualTo("line1")); + Assert.That(lines[1], Is.EqualTo("line2")); + } + + private void WriteToTempFile(string content) + { + File.WriteAllText(tempFilePath, content); + } + } +} diff --git a/TagCloudGeneratorTests/WordsFrequencyAnalyzerTests.cs b/TagCloudGeneratorTests/WordsFrequencyAnalyzerTests.cs new file mode 100644 index 00000000..f5eb2c55 --- /dev/null +++ b/TagCloudGeneratorTests/WordsFrequencyAnalyzerTests.cs @@ -0,0 +1,52 @@ +using NUnit.Framework; +using TagCloudGenerator.Infrastructure.Analyzers; + +namespace TagCloudGeneratorTests +{ + public class WordsFrequencyAnalyzerTests + { + private WordsFrequencyAnalyzer frequencyAnalyzer = new WordsFrequencyAnalyzer(); + + [Test] + public void Analyze_EmptyInput_ReturnsEmpty_Test() + { + var words = new List(); + var result = frequencyAnalyzer.Analyze(words); + Assert.That(result, Is.Empty); + } + + [Test] + public void Analyze_SingleWord_ReturnsOneItemWithFrequencyOne_Test() + { + var words = new List { "hello" }; + var result = frequencyAnalyzer.Analyze(words).ToList(); + + Assert.That(result.Count, Is.EqualTo(1)); + Assert.That(result[0].Key, Is.EqualTo("hello")); + Assert.That(result[0].Value, Is.EqualTo(1)); + } + + [Test] + public void Analyze_MultipleWords_ReturnsCorrectFrequencies_Test() + { + var words = new List { "hello", "world", "hello", "test", "world", "hello" }; + var result = frequencyAnalyzer.Analyze(words).ToList(); + + Assert.That(result.Count, Is.EqualTo(3)); + Assert.That(result[0].Key, Is.EqualTo("hello")); + Assert.That(result[0].Value, Is.EqualTo(3)); + Assert.That(result[1].Key, Is.EqualTo("world")); + Assert.That(result[1].Value, Is.EqualTo(2)); + Assert.That(result[2].Key, Is.EqualTo("test")); + Assert.That(result[2].Value, Is.EqualTo(1)); + } + + [Test] + public void Analyze_CaseSensitive_ReturnsSeparateItems_Test() + { + var words = new List { "Hello", "hello", "HELLO" }; + var result = frequencyAnalyzer.Analyze(words).ToList(); + Assert.That(result.Count, Is.EqualTo(3)); + } + } +} \ No newline at end of file diff --git a/TagCloudUIClient/MainForm.Designer.cs b/TagCloudUIClient/MainForm.Designer.cs new file mode 100644 index 00000000..4c062fdb --- /dev/null +++ b/TagCloudUIClient/MainForm.Designer.cs @@ -0,0 +1,485 @@ +using System.Drawing.Imaging; +using System.Drawing; +using TagCloudGenerator.Core.Interfaces; +using TagCloudGenerator.Core.Models; +using TagCloudGenerator.Infrastructure; +using TagCloudGenerator.Infrastructure.Filters; + +namespace TagCloudUIClient +{ + public partial class MainForm : Form + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + private readonly ITagCloudGenerator _generator; + private readonly IReaderRepository _readerRepository; + private readonly INormalizer _normalizer; + private string _lastOutputPath = string.Empty; + + private List wordsToRender; + + private Bitmap? _generatedBitmap; + + private Button btnChooseFile; + private Label lblFilePath; + private GroupBox groupBoxSettings; + private Label lblWidth; + private NumericUpDown numWidth; + private Label lblHeight; + private NumericUpDown numHeight; + private Label lblMinSize; + private NumericUpDown numMinSize; + private Label lblMaxSize; + private NumericUpDown numMaxSize; + private Button btnChooseFont; + private Label lblFont; + private Button btnTextColor; + private Button btnBgColor; + private CheckedListBox clbExcludedWords; + private Button btnGenerate; + private PictureBox picturePreview; + private Button btnSave; + + + public MainForm(ITagCloudGenerator generator, IReaderRepository readers, INormalizer normalizer) + { + _generator = generator; + _readerRepository = readers; + _normalizer = normalizer; + InitializeComponent(); + } + + private void btnChooseFile_Click(object sender, EventArgs e) + { + using var dlg = new OpenFileDialog(); + dlg.Filter = "Text files (*.txt)|*.txt|All files (*.*)|*.*"; + if (dlg.ShowDialog() == DialogResult.OK) + { + lblFilePath.Text = dlg.FileName; + LoadExcludedWords(dlg.FileName); + } + } + + private void LoadExcludedWords(string path) + { + clbExcludedWords.Items.Clear(); + _readerRepository.TryGetReader(path) + .Then(reader => reader.TryRead(path)) + .Then(words => _normalizer.Normalize(words)) + .OnSuccess(words => + { + wordsToRender = words; + }) + .OnFail(error => { + MessageBox.Show( + $"{error}", + "Error", + MessageBoxButtons.OK, + MessageBoxIcon.Warning); + return; + }); + + var text = File.ReadAllText(path); + var distinctWords = wordsToRender.Distinct().OrderBy(w => w); + + foreach (var w in distinctWords) + clbExcludedWords.Items.Add(w, true); + } + + private void btnChooseFont_Click(object sender, EventArgs e) + { + using var fontDlg = new FontDialog(); + fontDlg.Font = lblFont.Tag as Font ?? this.Font; + if (fontDlg.ShowDialog() == DialogResult.OK) + { + lblFont.Tag = fontDlg.Font; + lblFont.Text = $"{fontDlg.Font.Name}, {fontDlg.Font.Size}pt"; + } + } + + private void btnTextColor_Click(object sender, EventArgs e) + { + using var colorDlg = new ColorDialog(); + if (colorDlg.ShowDialog() == DialogResult.OK) + lblTextColor.BackColor = colorDlg.Color; + } + + private void btnBgColor_Click(object sender, EventArgs e) + { + using var colorDlg = new ColorDialog(); + if (colorDlg.ShowDialog() == DialogResult.OK) + lblBgColor.BackColor = colorDlg.Color; + } + + private void btnGenerate_Click(object sender, EventArgs e) + { + if (string.IsNullOrEmpty(lblFilePath.Text) || !File.Exists(lblFilePath.Text)) + { + MessageBox.Show( + "Пожалуйста, выберите корректный текстовый файл.", + "Ошибка", + MessageBoxButtons.OK, + MessageBoxIcon.Warning); + return; + } + + var allWords = clbExcludedWords.Items.Cast(); + + var excluded = allWords + .Where(word => !clbExcludedWords.CheckedItems.Contains(word)) + .ToList(); + + var filters = new List { new BoringWordsFilter(excluded) }; + + var font = lblFont.Tag as Font ?? Font; + var bgColor = lblBgColor.BackColor; + var textColor = lblTextColor.BackColor; + + var canvasSettings = new CanvasSettings() + .SetBackgroundColor(bgColor) + .SetWidth((int)numWidth.Value) + .SetHeight((int)numHeight.Value); + + var textSettings = new TextSettings() + .SetFontFamily(font.FontFamily.Name) + .SetMaxFontSize((int)numMaxSize.Value) + .SetMinFontSize((int)numMinSize.Value) + .SetTextColor(textColor); + + _generator.Generate(wordsToRender, canvasSettings, textSettings, filters) + .OnSuccess(bitmap => + { + picturePreview.Image?.Dispose(); + _generatedBitmap = bitmap; + picturePreview.Image = bitmap; + btnSave.Enabled = true; + }) + .OnFail(error => { + MessageBox.Show( + $"Generation error: {error}", + "Generation was not successful", + MessageBoxButtons.OK, + MessageBoxIcon.Error); + }); + } + + + private void btnSave_Click(object sender, EventArgs e) + { + if (_generatedBitmap == null) + return; + + using var saveDlg = new SaveFileDialog + { + Filter = "PNG Image|*.png|JPEG Image|*.jpg", + Title = "Сохранить облако тегов…", + FileName = "tagcloud.png" + }; + + if (saveDlg.ShowDialog() != DialogResult.OK) + return; + + var ext = Path.GetExtension(saveDlg.FileName).ToLowerInvariant(); + + var format = ext == ".jpg" || ext == ".jpeg" ? ImageFormat.Jpeg : ImageFormat.Png; + + _generatedBitmap.Save(saveDlg.FileName, format); + } + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + btnChooseFile = new Button(); + lblFilePath = new Label(); + groupBoxSettings = new GroupBox(); + lblWidth = new Label(); + numWidth = new NumericUpDown(); + lblHeight = new Label(); + numHeight = new NumericUpDown(); + lblMinSize = new Label(); + numMinSize = new NumericUpDown(); + lblMaxSize = new Label(); + numMaxSize = new NumericUpDown(); + btnChooseFont = new Button(); + lblFont = new Label(); + btnTextColor = new Button(); + btnBgColor = new Button(); + clbExcludedWords = new CheckedListBox(); + btnGenerate = new Button(); + picturePreview = new PictureBox(); + btnSave = new Button(); + lblBgColor = new Label(); + lblTextColor = new Label(); + groupBoxSettings.SuspendLayout(); + ((System.ComponentModel.ISupportInitialize)numWidth).BeginInit(); + ((System.ComponentModel.ISupportInitialize)numHeight).BeginInit(); + ((System.ComponentModel.ISupportInitialize)numMinSize).BeginInit(); + ((System.ComponentModel.ISupportInitialize)numMaxSize).BeginInit(); + ((System.ComponentModel.ISupportInitialize)picturePreview).BeginInit(); + SuspendLayout(); + // + // btnChooseFile + // + btnChooseFile.Location = new Point(12, 12); + btnChooseFile.Name = "btnChooseFile"; + btnChooseFile.Size = new Size(120, 30); + btnChooseFile.TabIndex = 0; + btnChooseFile.Text = "Выбрать файл..."; + btnChooseFile.UseVisualStyleBackColor = true; + btnChooseFile.Click += btnChooseFile_Click; + // + // lblFilePath + // + lblFilePath.AutoSize = true; + lblFilePath.Location = new Point(150, 20); + lblFilePath.Name = "lblFilePath"; + lblFilePath.Size = new Size(124, 20); + lblFilePath.TabIndex = 1; + lblFilePath.Text = "Файл не выбран"; + // + // groupBoxSettings + // + groupBoxSettings.Controls.Add(lblWidth); + groupBoxSettings.Controls.Add(numWidth); + groupBoxSettings.Controls.Add(lblHeight); + groupBoxSettings.Controls.Add(numHeight); + groupBoxSettings.Controls.Add(lblMinSize); + groupBoxSettings.Controls.Add(numMinSize); + groupBoxSettings.Controls.Add(lblMaxSize); + groupBoxSettings.Controls.Add(numMaxSize); + groupBoxSettings.Controls.Add(btnChooseFont); + groupBoxSettings.Controls.Add(lblFont); + groupBoxSettings.Location = new Point(12, 60); + groupBoxSettings.Name = "groupBoxSettings"; + groupBoxSettings.Size = new Size(380, 180); + groupBoxSettings.TabIndex = 2; + groupBoxSettings.TabStop = false; + groupBoxSettings.Text = "Настройки генерации"; + // + // lblWidth + // + lblWidth.AutoSize = true; + lblWidth.Location = new Point(10, 25); + lblWidth.Name = "lblWidth"; + lblWidth.Size = new Size(70, 20); + lblWidth.TabIndex = 0; + lblWidth.Text = "Ширина:"; + // + // numWidth + // + numWidth.Location = new Point(80, 23); + numWidth.Maximum = new decimal(new int[] { 4000, 0, 0, 0 }); + numWidth.Minimum = new decimal(new int[] { 100, 0, 0, 0 }); + numWidth.Name = "numWidth"; + numWidth.Size = new Size(80, 27); + numWidth.TabIndex = 1; + numWidth.Value = new decimal(new int[] { 800, 0, 0, 0 }); + // + // lblHeight + // + lblHeight.AutoSize = true; + lblHeight.Location = new Point(180, 25); + lblHeight.Name = "lblHeight"; + lblHeight.Size = new Size(62, 20); + lblHeight.TabIndex = 2; + lblHeight.Text = "Высота:"; + // + // numHeight + // + numHeight.Location = new Point(250, 23); + numHeight.Maximum = new decimal(new int[] { 4000, 0, 0, 0 }); + numHeight.Minimum = new decimal(new int[] { 100, 0, 0, 0 }); + numHeight.Name = "numHeight"; + numHeight.Size = new Size(80, 27); + numHeight.TabIndex = 3; + numHeight.Value = new decimal(new int[] { 600, 0, 0, 0 }); + // + // lblMinSize + // + lblMinSize.AutoSize = true; + lblMinSize.Location = new Point(10, 60); + lblMinSize.Name = "lblMinSize"; + lblMinSize.Size = new Size(218, 20); + lblMinSize.TabIndex = 4; + lblMinSize.Text = "Минимальный размер текста:"; + // + // numMinSize + // + numMinSize.Location = new Point(250, 58); + numMinSize.Maximum = new decimal(new int[] { 200, 0, 0, 0 }); + numMinSize.Minimum = new decimal(new int[] { 1, 0, 0, 0 }); + numMinSize.Name = "numMinSize"; + numMinSize.Size = new Size(60, 27); + numMinSize.TabIndex = 5; + numMinSize.Value = new decimal(new int[] { 10, 0, 0, 0 }); + // + // lblMaxSize + // + lblMaxSize.AutoSize = true; + lblMaxSize.Location = new Point(10, 95); + lblMaxSize.Name = "lblMaxSize"; + lblMaxSize.Size = new Size(222, 20); + lblMaxSize.TabIndex = 6; + lblMaxSize.Text = "Максимальный размер текста:"; + // + // numMaxSize + // + numMaxSize.Location = new Point(250, 93); + numMaxSize.Maximum = new decimal(new int[] { 200, 0, 0, 0 }); + numMaxSize.Minimum = new decimal(new int[] { 1, 0, 0, 0 }); + numMaxSize.Name = "numMaxSize"; + numMaxSize.Size = new Size(60, 27); + numMaxSize.TabIndex = 7; + numMaxSize.Value = new decimal(new int[] { 80, 0, 0, 0 }); + numMaxSize.ValueChanged += numMaxSize_ValueChanged; + // + // btnChooseFont + // + btnChooseFont.Location = new Point(10, 135); + btnChooseFont.Name = "btnChooseFont"; + btnChooseFont.Size = new Size(120, 25); + btnChooseFont.TabIndex = 8; + btnChooseFont.Text = "Выбрать шрифт..."; + btnChooseFont.Click += btnChooseFont_Click; + // + // lblFont + // + lblFont.AutoSize = true; + lblFont.Location = new Point(150, 135); + lblFont.Name = "lblFont"; + lblFont.Size = new Size(144, 20); + lblFont.TabIndex = 9; + lblFont.Text = "(шрифт не выбран)"; + // + // btnTextColor + // + btnTextColor.Location = new Point(22, 246); + btnTextColor.Name = "btnTextColor"; + btnTextColor.Size = new Size(120, 25); + btnTextColor.TabIndex = 10; + btnTextColor.Text = "Цвет текста..."; + btnTextColor.Click += btnTextColor_Click; + // + // btnBgColor + // + btnBgColor.Location = new Point(22, 290); + btnBgColor.Name = "btnBgColor"; + btnBgColor.Size = new Size(120, 25); + btnBgColor.TabIndex = 12; + btnBgColor.Text = "Цвет фона..."; + btnBgColor.Click += btnBgColor_Click; + // + // clbExcludedWords + // + clbExcludedWords.CheckOnClick = true; + clbExcludedWords.FormattingEnabled = true; + clbExcludedWords.Location = new Point(22, 338); + clbExcludedWords.Name = "clbExcludedWords"; + clbExcludedWords.Size = new Size(250, 180); + clbExcludedWords.TabIndex = 3; + // + // btnGenerate + // + btnGenerate.Location = new Point(22, 524); + btnGenerate.Name = "btnGenerate"; + btnGenerate.Size = new Size(139, 30); + btnGenerate.TabIndex = 4; + btnGenerate.Text = "Сгенерировать"; + btnGenerate.Click += btnGenerate_Click; + // + // picturePreview + // + picturePreview.BorderStyle = BorderStyle.FixedSingle; + picturePreview.Location = new Point(410, 60); + picturePreview.Name = "picturePreview"; + picturePreview.Size = new Size(688, 534); + picturePreview.SizeMode = PictureBoxSizeMode.Zoom; + picturePreview.TabIndex = 5; + picturePreview.TabStop = false; + // + // btnSave + // + btnSave.Enabled = false; + btnSave.Location = new Point(164, 524); + btnSave.Name = "btnSave"; + btnSave.Size = new Size(110, 30); + btnSave.TabIndex = 6; + btnSave.Text = "Сохранить..."; + btnSave.Click += btnSave_Click; + // + // lblBgColor + // + lblBgColor.AutoSize = true; + lblBgColor.BackColor = Color.White; + lblBgColor.BorderStyle = BorderStyle.FixedSingle; + lblBgColor.Location = new Point(162, 290); + lblBgColor.Name = "lblBgColor"; + lblBgColor.Size = new Size(2, 22); + lblBgColor.TabIndex = 13; + // + // lblTextColor + // + lblTextColor.AutoSize = true; + lblTextColor.BackColor = Color.Black; + lblTextColor.BorderStyle = BorderStyle.FixedSingle; + lblTextColor.Location = new Point(162, 249); + lblTextColor.Name = "lblTextColor"; + lblTextColor.Size = new Size(2, 22); + lblTextColor.TabIndex = 11; + // + // MainForm + // + AutoScaleDimensions = new SizeF(8F, 20F); + AutoScaleMode = AutoScaleMode.Font; + ClientSize = new Size(1110, 660); + Controls.Add(btnChooseFile); + Controls.Add(lblFilePath); + Controls.Add(groupBoxSettings); + Controls.Add(clbExcludedWords); + Controls.Add(btnGenerate); + Controls.Add(picturePreview); + Controls.Add(btnSave); + Controls.Add(btnTextColor); + Controls.Add(btnBgColor); + Controls.Add(lblBgColor); + Controls.Add(lblTextColor); + Name = "MainForm"; + Text = "Генератор облака тегов"; + groupBoxSettings.ResumeLayout(false); + groupBoxSettings.PerformLayout(); + ((System.ComponentModel.ISupportInitialize)numWidth).EndInit(); + ((System.ComponentModel.ISupportInitialize)numHeight).EndInit(); + ((System.ComponentModel.ISupportInitialize)numMinSize).EndInit(); + ((System.ComponentModel.ISupportInitialize)numMaxSize).EndInit(); + ((System.ComponentModel.ISupportInitialize)picturePreview).EndInit(); + ResumeLayout(false); + PerformLayout(); + } + + #endregion + + private Label lblBgColor; + private Label lblTextColor; + } +} \ No newline at end of file diff --git a/TagCloudUIClient/MainForm.cs b/TagCloudUIClient/MainForm.cs new file mode 100644 index 00000000..42694581 --- /dev/null +++ b/TagCloudUIClient/MainForm.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Data; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace TagCloudUIClient +{ + public partial class MainForm : Form + { + public MainForm() + { + InitializeComponent(); + } + + private void MainForm_Load(object sender, EventArgs e) + { + + } + + private void numMaxSize_ValueChanged(object sender, EventArgs e) + { + + } + } +} \ No newline at end of file diff --git a/TagCloudUIClient/MainForm.resx b/TagCloudUIClient/MainForm.resx new file mode 100644 index 00000000..1af7de15 --- /dev/null +++ b/TagCloudUIClient/MainForm.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/TagCloudUIClient/Program.cs b/TagCloudUIClient/Program.cs new file mode 100644 index 00000000..650b4211 --- /dev/null +++ b/TagCloudUIClient/Program.cs @@ -0,0 +1,29 @@ +using Autofac; +using TagCloudGenerator.Core.Interfaces; +using TagCloudGenerator.DI; + +namespace TagCloudUIClient +{ + internal static class Program + { + /// + /// The main entry point for the application. + /// + [STAThread] + static void Main() + { + // To customize application configuration such as set high DPI settings or default font, + // see https://aka.ms/applicationconfiguration. + ApplicationConfiguration.Initialize(); + + var builder = new ContainerBuilder(); + builder.RegisterModule(); + builder.RegisterType().As().SingleInstance(); + var container = builder.Build(); + + using var scope = container.BeginLifetimeScope(); + var client = scope.Resolve(); + client.Run(Array.Empty()); + } + } +} \ No newline at end of file diff --git a/TagCloudUIClient/TagCloudUIClient.csproj b/TagCloudUIClient/TagCloudUIClient.csproj new file mode 100644 index 00000000..f78d3be6 --- /dev/null +++ b/TagCloudUIClient/TagCloudUIClient.csproj @@ -0,0 +1,15 @@ + + + + WinExe + net8.0-windows + enable + true + enable + + + + + + + \ No newline at end of file diff --git a/TagCloudUIClient/WinFormsClient.cs b/TagCloudUIClient/WinFormsClient.cs new file mode 100644 index 00000000..c0ffe1b5 --- /dev/null +++ b/TagCloudUIClient/WinFormsClient.cs @@ -0,0 +1,27 @@ +using TagCloudGenerator.Core.Interfaces; + +namespace TagCloudUIClient +{ + public class WinFormsClient : IClient + { + private readonly ITagCloudGenerator _generator; + private readonly IReaderRepository _reader; + private readonly INormalizer _normalizer; + + public WinFormsClient(ITagCloudGenerator generator, IReaderRepository repository, INormalizer normalizer) + { + _generator = generator; + _reader = repository; + _normalizer = normalizer; + } + + public void Run(string[] args) + { + Application.EnableVisualStyles(); + Application.SetCompatibleTextRenderingDefault(false); + + var mainForm = new MainForm(_generator, _reader, _normalizer); + Application.Run(mainForm); + } + } +} diff --git a/di.sln b/di.sln index a50991da..533814f3 100644 --- a/di.sln +++ b/di.sln @@ -1,6 +1,17 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "di", "FractalPainter/di.csproj", "{4B70F6B3-5C20-40D2-BFC9-D95C18D65DD6}" +# Visual Studio Version 17 +VisualStudioVersion = 17.13.35919.96 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "di", "FractalPainter\di.csproj", "{4B70F6B3-5C20-40D2-BFC9-D95C18D65DD6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TagCloudGenerator", "TagCloudGenerator\TagCloudGenerator.csproj", "{7F81F0E1-386F-4ACE-A723-5AEDE6DF34A9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TagCloudGeneratorTests", "TagCloudGeneratorTests\TagCloudGeneratorTests.csproj", "{235C2379-72BC-4BF2-994D-B5DA4146AB8D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TagCloudConsoleClient", "TagCloudConsoleClient\TagCloudConsoleClient.csproj", "{C8F88DBA-BFBC-4E4F-B305-E7EB928103AD}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TagCloudUIClient", "TagCloudUIClient\TagCloudUIClient.csproj", "{62B780CA-0CCB-4F52-B5BD-E294E53677BA}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -12,5 +23,27 @@ Global {4B70F6B3-5C20-40D2-BFC9-D95C18D65DD6}.Debug|Any CPU.Build.0 = Debug|Any CPU {4B70F6B3-5C20-40D2-BFC9-D95C18D65DD6}.Release|Any CPU.ActiveCfg = Release|Any CPU {4B70F6B3-5C20-40D2-BFC9-D95C18D65DD6}.Release|Any CPU.Build.0 = Release|Any CPU + {7F81F0E1-386F-4ACE-A723-5AEDE6DF34A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7F81F0E1-386F-4ACE-A723-5AEDE6DF34A9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7F81F0E1-386F-4ACE-A723-5AEDE6DF34A9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7F81F0E1-386F-4ACE-A723-5AEDE6DF34A9}.Release|Any CPU.Build.0 = Release|Any CPU + {235C2379-72BC-4BF2-994D-B5DA4146AB8D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {235C2379-72BC-4BF2-994D-B5DA4146AB8D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {235C2379-72BC-4BF2-994D-B5DA4146AB8D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {235C2379-72BC-4BF2-994D-B5DA4146AB8D}.Release|Any CPU.Build.0 = Release|Any CPU + {C8F88DBA-BFBC-4E4F-B305-E7EB928103AD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C8F88DBA-BFBC-4E4F-B305-E7EB928103AD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C8F88DBA-BFBC-4E4F-B305-E7EB928103AD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C8F88DBA-BFBC-4E4F-B305-E7EB928103AD}.Release|Any CPU.Build.0 = Release|Any CPU + {62B780CA-0CCB-4F52-B5BD-E294E53677BA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {62B780CA-0CCB-4F52-B5BD-E294E53677BA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {62B780CA-0CCB-4F52-B5BD-E294E53677BA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {62B780CA-0CCB-4F52-B5BD-E294E53677BA}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {BFEBD95D-6AE8-4FB5-A3F5-52EAB5899DAC} EndGlobalSection EndGlobal