From 8e0452a8f5da36a9cb17cbb1fe31ebc491c8817b Mon Sep 17 00:00:00 2001 From: Krotkaya Date: Fri, 16 Jan 2026 16:38:29 +0500 Subject: [PATCH 1/6] =?UTF-8?q?=D0=A0=D0=B5=D1=88=D0=B5=D0=BD=D0=B8=D0=B5?= =?UTF-8?q?=20=D0=B7=D0=B0=D0=B4=D0=B0=D1=87=D0=B8=20=D0=B8=D0=B7=20=D0=B1?= =?UTF-8?q?=D0=BB=D0=BE=D0=BA=D0=B0=20Dependency=20Injection=20Container?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit На его основе буду выполнять следующее задание --- .../CommandLineOptions.cs | 38 +++++++ TagsCloudContainer.Console/ConsoleClient.cs | 41 +++++++ TagsCloudContainer.Console/Program.cs | 58 ++++++++++ .../TagsCloudContainer.Console.csproj | 14 +++ TagsCloudContainer.Core/.DS_Store | Bin 0 -> 6148 bytes TagsCloudContainer.Core/DI/AutofacModule.cs | 70 ++++++++++++ .../DI/CompositePreprocessor.cs | 11 ++ .../Infrastructure/.DS_Store | Bin 0 -> 6148 bytes .../Analysis/IWordFrequencyAnalyzer.cs | 8 ++ .../Analysis/WordFrequencyAnalyzer.cs | 15 +++ .../Coloring/ColorSchemeType.cs | 7 ++ .../Coloring/GradientColorScheme.cs | 22 ++++ .../Infrastructure/Coloring/IColorScheme.cs | 7 ++ .../Coloring/RandomColorScheme.cs | 22 ++++ .../Layout/ArchimedeanSpiralPointGenerator.cs | 19 ++++ .../Layout/IFontSizeCalculator.cs | 6 + .../Layout/ITagCloudAlgorithm.cs | 9 ++ .../Layout/LinearFontSizeCalculator.cs | 15 +++ .../Layout/RectangleCloudLayouter.cs | 69 ++++++++++++ .../Layout/SpiralTagCloudAlgorithm.cs | 90 +++++++++++++++ .../Layout/TagCloudAlgorithmType.cs | 7 ++ .../Layout/TightSpiralTagCloudAlgorithm.cs | 48 ++++++++ .../Preprocessing/BoringWordsFilter.cs | 41 +++++++ .../Preprocessing/EmptyBoringWordsProvider.cs | 6 + .../Preprocessing/FileBoringWordsProvider.cs | 6 + .../Preprocessing/IBoringWordsProvider.cs | 6 + .../Preprocessing/ITextPreprocessor.cs | 5 + .../Preprocessing/IWordFilter.cs | 5 + .../Preprocessing/LowerCaseNormalizer.cs | 8 ++ .../Infrastructure/Reading/DocxTextReader.cs | 30 +++++ .../Infrastructure/Reading/ITextReader.cs | 10 ++ .../Reading/MultiFormatTextReader.cs | 16 +++ .../Infrastructure/Reading/TextFileReader.cs | 18 +++ .../Rendering/ITagCloudRenderer.cs | 9 ++ .../Infrastructure/Rendering/PngRenderer.cs | 38 +++++++ .../Models/LayoutOptions.cs | 16 +++ TagsCloudContainer.Core/Models/LayoutWord.cs | 21 ++++ .../Models/TagCloudSettings.cs | 15 +++ .../Models/WordFrequency.cs | 13 +++ .../TagsCloudContainer.Core.csproj | 18 +++ TagsCloudContainer.Tests/PngRendererTests.cs | 44 ++++++++ .../SpiralTagCloudAlgorithmTests.cs | 68 +++++++++++ .../TagsCloudContainer.Tests.csproj | 20 ++++ .../TextFileReaderTests.cs | 74 ++++++++++++ TagsCloudContainer.Tests/words.txt | 106 ++++++++++++++++++ fp.sln | 18 +++ out.png | Bin 0 -> 140748 bytes words.txt | 106 ++++++++++++++++++ 48 files changed, 1293 insertions(+) create mode 100644 TagsCloudContainer.Console/CommandLineOptions.cs create mode 100644 TagsCloudContainer.Console/ConsoleClient.cs create mode 100644 TagsCloudContainer.Console/Program.cs create mode 100644 TagsCloudContainer.Console/TagsCloudContainer.Console.csproj create mode 100644 TagsCloudContainer.Core/.DS_Store create mode 100644 TagsCloudContainer.Core/DI/AutofacModule.cs create mode 100644 TagsCloudContainer.Core/DI/CompositePreprocessor.cs create mode 100644 TagsCloudContainer.Core/Infrastructure/.DS_Store create mode 100644 TagsCloudContainer.Core/Infrastructure/Analysis/IWordFrequencyAnalyzer.cs create mode 100644 TagsCloudContainer.Core/Infrastructure/Analysis/WordFrequencyAnalyzer.cs create mode 100644 TagsCloudContainer.Core/Infrastructure/Coloring/ColorSchemeType.cs create mode 100644 TagsCloudContainer.Core/Infrastructure/Coloring/GradientColorScheme.cs create mode 100644 TagsCloudContainer.Core/Infrastructure/Coloring/IColorScheme.cs create mode 100644 TagsCloudContainer.Core/Infrastructure/Coloring/RandomColorScheme.cs create mode 100644 TagsCloudContainer.Core/Infrastructure/Layout/ArchimedeanSpiralPointGenerator.cs create mode 100644 TagsCloudContainer.Core/Infrastructure/Layout/IFontSizeCalculator.cs create mode 100644 TagsCloudContainer.Core/Infrastructure/Layout/ITagCloudAlgorithm.cs create mode 100644 TagsCloudContainer.Core/Infrastructure/Layout/LinearFontSizeCalculator.cs create mode 100644 TagsCloudContainer.Core/Infrastructure/Layout/RectangleCloudLayouter.cs create mode 100644 TagsCloudContainer.Core/Infrastructure/Layout/SpiralTagCloudAlgorithm.cs create mode 100644 TagsCloudContainer.Core/Infrastructure/Layout/TagCloudAlgorithmType.cs create mode 100644 TagsCloudContainer.Core/Infrastructure/Layout/TightSpiralTagCloudAlgorithm.cs create mode 100644 TagsCloudContainer.Core/Infrastructure/Preprocessing/BoringWordsFilter.cs create mode 100644 TagsCloudContainer.Core/Infrastructure/Preprocessing/EmptyBoringWordsProvider.cs create mode 100644 TagsCloudContainer.Core/Infrastructure/Preprocessing/FileBoringWordsProvider.cs create mode 100644 TagsCloudContainer.Core/Infrastructure/Preprocessing/IBoringWordsProvider.cs create mode 100644 TagsCloudContainer.Core/Infrastructure/Preprocessing/ITextPreprocessor.cs create mode 100644 TagsCloudContainer.Core/Infrastructure/Preprocessing/IWordFilter.cs create mode 100644 TagsCloudContainer.Core/Infrastructure/Preprocessing/LowerCaseNormalizer.cs create mode 100644 TagsCloudContainer.Core/Infrastructure/Reading/DocxTextReader.cs create mode 100644 TagsCloudContainer.Core/Infrastructure/Reading/ITextReader.cs create mode 100644 TagsCloudContainer.Core/Infrastructure/Reading/MultiFormatTextReader.cs create mode 100644 TagsCloudContainer.Core/Infrastructure/Reading/TextFileReader.cs create mode 100644 TagsCloudContainer.Core/Infrastructure/Rendering/ITagCloudRenderer.cs create mode 100644 TagsCloudContainer.Core/Infrastructure/Rendering/PngRenderer.cs create mode 100644 TagsCloudContainer.Core/Models/LayoutOptions.cs create mode 100644 TagsCloudContainer.Core/Models/LayoutWord.cs create mode 100644 TagsCloudContainer.Core/Models/TagCloudSettings.cs create mode 100644 TagsCloudContainer.Core/Models/WordFrequency.cs create mode 100644 TagsCloudContainer.Core/TagsCloudContainer.Core.csproj create mode 100644 TagsCloudContainer.Tests/PngRendererTests.cs create mode 100644 TagsCloudContainer.Tests/SpiralTagCloudAlgorithmTests.cs create mode 100644 TagsCloudContainer.Tests/TagsCloudContainer.Tests.csproj create mode 100644 TagsCloudContainer.Tests/TextFileReaderTests.cs create mode 100644 TagsCloudContainer.Tests/words.txt create mode 100644 out.png create mode 100644 words.txt diff --git a/TagsCloudContainer.Console/CommandLineOptions.cs b/TagsCloudContainer.Console/CommandLineOptions.cs new file mode 100644 index 000000000..6704141d0 --- /dev/null +++ b/TagsCloudContainer.Console/CommandLineOptions.cs @@ -0,0 +1,38 @@ +using CommandLine; +using TagsCloudContainer.Core.Infrastructure.Coloring; +using TagsCloudContainer.Core.Infrastructure.Layout; + +namespace TagsCloudContainer.Console; +public class CommandLineOptions +{ + [Option('i', "input", Required = true, HelpText = "Input file path")] + public string InputFile { get; init; } = string.Empty; + + [Option('o', "output", Required = true, HelpText = "Output file path")] + public string OutputFile { get; init; } = string.Empty; + + [Option('w', "width", Default = 800, HelpText = "Image width")] + public int Width { get; init; } + + [Option('h', "height", Default = 600, HelpText = "Image height")] + public int Height { get; init; } + + [Option('f', "font", Default = "Arial", HelpText = "Font family")] + public string FontFamily { get; init; } = string.Empty; + + [Option('c', "colors", HelpText = "Color scheme (Random/Gradient)")] + public ColorSchemeType ColorScheme { get; init; } = ColorSchemeType.Random; + + [Option("boring-words", HelpText = "Path to file with boring words")] + public string? BoringWordsFile { get; init; } + + [Option("min-font", Default = 10, HelpText = "Minimum font size")] + public int MinFontSize { get; init; } + + [Option("max-font", Default = 50, HelpText = "Maximum font size")] + public int MaxFontSize { get; init; } + + [Option("algorithm", Default = TagCloudAlgorithmType.Spiral, HelpText = + "Algorithm: Spiral | Tight")] + public TagCloudAlgorithmType Algorithm { get; init; } = TagCloudAlgorithmType.Spiral; +} diff --git a/TagsCloudContainer.Console/ConsoleClient.cs b/TagsCloudContainer.Console/ConsoleClient.cs new file mode 100644 index 000000000..55a04cb09 --- /dev/null +++ b/TagsCloudContainer.Console/ConsoleClient.cs @@ -0,0 +1,41 @@ +using TagsCloudContainer.Core.Infrastructure.Analysis; +using TagsCloudContainer.Core.Infrastructure.Layout; +using TagsCloudContainer.Core.Infrastructure.Preprocessing; +using TagsCloudContainer.Core.Infrastructure.Reading; +using TagsCloudContainer.Core.Infrastructure.Rendering; +using TagsCloudContainer.Core.Models; + +namespace TagsCloudContainer.Console; +public class ConsoleClient( + ITextReader textReader, + ITextPreprocessor preprocessor, + IWordFrequencyAnalyzer analyzer, + ITagCloudAlgorithm algorithm, + ITagCloudRenderer renderer) +{ + public void GenerateTagCloud( + string inputFile, + string outputFile, + LayoutOptions options) + { + System.Console.WriteLine($"Reading words from {inputFile}..."); + var words = textReader.ReadWords(inputFile).ToArray(); + + System.Console.WriteLine("Processing words..."); + var processedWords = preprocessor.Process(words).ToArray(); + + System.Console.WriteLine("Analyzing word frequencies..."); + var frequencies = analyzer.Analyze(processedWords).ToArray(); + + System.Console.WriteLine("Arranging words in cloud..."); + var layoutWords = algorithm.Arrange(frequencies, options).ToArray(); + + System.Console.WriteLine($"Rendering image ({options.Width}x{options.Height})..."); + var image = renderer.Render(layoutWords, options); + + renderer.SaveToFile(image, outputFile); + + System.Console.WriteLine($"Tag cloud saved to {outputFile}"); + System.Console.WriteLine($"Total words processed: {frequencies.Length}"); + } +} \ No newline at end of file diff --git a/TagsCloudContainer.Console/Program.cs b/TagsCloudContainer.Console/Program.cs new file mode 100644 index 000000000..4c63e78fc --- /dev/null +++ b/TagsCloudContainer.Console/Program.cs @@ -0,0 +1,58 @@ +using Autofac; +using CommandLine; +using TagsCloudContainer.Console; +using TagsCloudContainer.Core.DI; +using TagsCloudContainer.Core.Models; + +var parser = new Parser(with => with.HelpWriter = Console.Out); + +return parser.ParseArguments(args) + .MapResult( + options => + { + try + { + var settings = new TagCloudSettings + { + Width = options.Width, + Height = options.Height, + FontFamily = options.FontFamily, + ColorScheme = options.ColorScheme, + MinFontSize = options.MinFontSize, + MaxFontSize = options.MaxFontSize, + Algorithm = options.Algorithm, + BoringWordsPath = options.BoringWordsFile + }; + + var builder = new ContainerBuilder(); + builder.RegisterModule(new AutofacModule(settings)); + builder.RegisterInstance(options).As(); + builder.RegisterType().SingleInstance(); + + var container = builder.Build(); + + var layoutOptions = new LayoutOptions + { + Width = options.Width, + Height = options.Height, + FontFamily = options.FontFamily + }; + + var client = container.Resolve(); + client.GenerateTagCloud(options.InputFile, options.OutputFile, layoutOptions); + + Console.WriteLine("Tag cloud generated successfully!"); + return 0; + } + catch (Exception ex) + { + Console.Error.WriteLine($"Error: {ex.Message}"); + return 1; + } + }, + errors => + { + foreach (var e in errors) + Console.Error.WriteLine(e.ToString()); + return 1; + }); \ No newline at end of file diff --git a/TagsCloudContainer.Console/TagsCloudContainer.Console.csproj b/TagsCloudContainer.Console/TagsCloudContainer.Console.csproj new file mode 100644 index 000000000..a3547b7fd --- /dev/null +++ b/TagsCloudContainer.Console/TagsCloudContainer.Console.csproj @@ -0,0 +1,14 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + diff --git a/TagsCloudContainer.Core/.DS_Store b/TagsCloudContainer.Core/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..6948305f873a76e103ff31299981f63b94a434a8 GIT binary patch literal 6148 zcmeHKyG{c^3>-s>NJvpi%KZiYU=@XenjZiXs3Ice(O<=P@o5=93ZjE5qKU?mcXquV zpKglt8GtR0>jz*CU`ltymoH=Ud-svuRK$pM)_BDp2kdafYLxwZz_~L#;S(R^{p255 zWrz3mezkeqZa1DNlLAse3P=GdAO$W~pjztm?&3-+AO)nrwJG4=hemhog+pR|I=I9L zK%6if#(m5Z#O48FFB}pXp;=OiNwpd=Ea{B5%Ik$gV$xwXd{{l%YC^GiI_tMchxJ6Q zQa}ovD{z~~x%dA^`XBTEIY}oeAO)^U0h=tJmkYj9_14kLd9Q8s7rN(s)7`ia3YTcd k#AwGncsst2q|9r+=Y2065`)fo(24pPa9w0l;J+0(0VtIhVgLXD literal 0 HcmV?d00001 diff --git a/TagsCloudContainer.Core/DI/AutofacModule.cs b/TagsCloudContainer.Core/DI/AutofacModule.cs new file mode 100644 index 000000000..d7c2015a3 --- /dev/null +++ b/TagsCloudContainer.Core/DI/AutofacModule.cs @@ -0,0 +1,70 @@ +using Autofac; +using SkiaSharp; +using TagsCloudContainer.Core.Infrastructure.Analysis; +using TagsCloudContainer.Core.Infrastructure.Coloring; +using TagsCloudContainer.Core.Infrastructure.Layout; +using TagsCloudContainer.Core.Infrastructure.Preprocessing; +using TagsCloudContainer.Core.Infrastructure.Reading; +using TagsCloudContainer.Core.Infrastructure.Rendering; +using TagsCloudContainer.Core.Models; + +namespace TagsCloudContainer.Core.DI; +public class AutofacModule(TagCloudSettings settings) : Module +{ + protected override void Load(ContainerBuilder builder) + { + builder.RegisterInstance(settings).As(); + + builder.RegisterType().As(); + builder.RegisterType().As(); + + builder.RegisterType() + .As().SingleInstance(); + + builder.Register(_ => + string.IsNullOrWhiteSpace(settings.BoringWordsPath) + ? new EmptyBoringWordsProvider() + : new FileBoringWordsProvider(settings.BoringWordsPath)) + .SingleInstance(); + + builder.Register(c => + { + var boringWords = c.Resolve().GetWords(); + return new CompositePreprocessor([ + new LowerCaseNormalizer(), + new BoringWordsFilter(boringWords) + ]); + }).SingleInstance(); + + builder.RegisterType().As().SingleInstance(); + + builder.Register(_ + => new LinearFontSizeCalculator(settings.MinFontSize, settings.MaxFontSize)).SingleInstance(); + + builder.RegisterType() + .Keyed(ColorSchemeType.Random) + .SingleInstance(); + + builder.Register(_ => new GradientColorScheme(SKColors.Blue, SKColors.LightBlue)) + .Keyed(ColorSchemeType.Gradient) + .SingleInstance(); + + builder.Register(ctx => + ctx.ResolveKeyed(settings.ColorScheme)) + .SingleInstance(); + + builder.RegisterType() + .Keyed(TagCloudAlgorithmType.Spiral) + .SingleInstance(); + + builder.RegisterType() + .Keyed(TagCloudAlgorithmType.Tight) + .SingleInstance(); + + builder.Register(ctx => + ctx.ResolveKeyed(settings.Algorithm)) + .SingleInstance(); + + builder.RegisterType().As().SingleInstance(); + } +} diff --git a/TagsCloudContainer.Core/DI/CompositePreprocessor.cs b/TagsCloudContainer.Core/DI/CompositePreprocessor.cs new file mode 100644 index 000000000..9ae307aca --- /dev/null +++ b/TagsCloudContainer.Core/DI/CompositePreprocessor.cs @@ -0,0 +1,11 @@ +using TagsCloudContainer.Core.Infrastructure.Preprocessing; + +namespace TagsCloudContainer.Core.DI; +public class CompositePreprocessor(IEnumerable preprocessors) : ITextPreprocessor +{ + public IReadOnlyList Process(IEnumerable words) + { + return preprocessors.Aggregate(words, (current, preprocessor) + => preprocessor.Process(current)).ToArray(); + } +} \ No newline at end of file diff --git a/TagsCloudContainer.Core/Infrastructure/.DS_Store b/TagsCloudContainer.Core/Infrastructure/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..168115619ce59e7ccacecb19d0dae8f76e234f16 GIT binary patch literal 6148 zcmeHK%Sr=55Ukc50wUz-ael!+7(%=Y{(zVS5rQj*-1p>n`Ds=^F3Tc#h?h{k^wdmm z*9=pK?QH z7!Kn+dI@6l0I?U2iHy)Jsl=pOwHTIk##`m}!Z9)FuxdW6PPXb$EKcYCEz)5R|3Lqx|DTeyk^)lTq7<;j=4rF$m8!PRF6Xtj(Vyv_^G$c-JSZHZ9227) hbK&LqE|M~@`JC^2;g}e7#)D4O&w%S9lLG&(zz=EO7U2K@ literal 0 HcmV?d00001 diff --git a/TagsCloudContainer.Core/Infrastructure/Analysis/IWordFrequencyAnalyzer.cs b/TagsCloudContainer.Core/Infrastructure/Analysis/IWordFrequencyAnalyzer.cs new file mode 100644 index 000000000..136994403 --- /dev/null +++ b/TagsCloudContainer.Core/Infrastructure/Analysis/IWordFrequencyAnalyzer.cs @@ -0,0 +1,8 @@ +using TagsCloudContainer.Core.Models; + +namespace TagsCloudContainer.Core.Infrastructure.Analysis; + +public interface IWordFrequencyAnalyzer +{ + IReadOnlyList Analyze(IEnumerable words); +} \ No newline at end of file diff --git a/TagsCloudContainer.Core/Infrastructure/Analysis/WordFrequencyAnalyzer.cs b/TagsCloudContainer.Core/Infrastructure/Analysis/WordFrequencyAnalyzer.cs new file mode 100644 index 000000000..6fd96e5f0 --- /dev/null +++ b/TagsCloudContainer.Core/Infrastructure/Analysis/WordFrequencyAnalyzer.cs @@ -0,0 +1,15 @@ +using TagsCloudContainer.Core.Models; + +namespace TagsCloudContainer.Core.Infrastructure.Analysis; +public class WordFrequencyAnalyzer : IWordFrequencyAnalyzer +{ + public IReadOnlyList Analyze(IEnumerable words) + { + var frequencies = words + .GroupBy(word => word) + .Select(group + => new WordFrequency(group.Key, group.Count())) + .OrderByDescending(wf => wf.Frequency).ToArray(); + return frequencies; + } +} \ No newline at end of file diff --git a/TagsCloudContainer.Core/Infrastructure/Coloring/ColorSchemeType.cs b/TagsCloudContainer.Core/Infrastructure/Coloring/ColorSchemeType.cs new file mode 100644 index 000000000..d8788a1d9 --- /dev/null +++ b/TagsCloudContainer.Core/Infrastructure/Coloring/ColorSchemeType.cs @@ -0,0 +1,7 @@ +namespace TagsCloudContainer.Core.Infrastructure.Coloring; + +public enum ColorSchemeType +{ + Random, + Gradient +} \ No newline at end of file diff --git a/TagsCloudContainer.Core/Infrastructure/Coloring/GradientColorScheme.cs b/TagsCloudContainer.Core/Infrastructure/Coloring/GradientColorScheme.cs new file mode 100644 index 000000000..fc3aee720 --- /dev/null +++ b/TagsCloudContainer.Core/Infrastructure/Coloring/GradientColorScheme.cs @@ -0,0 +1,22 @@ +using SkiaSharp; + +namespace TagsCloudContainer.Core.Infrastructure.Coloring; +public class GradientColorScheme(SKColor startColor, SKColor endColor) : IColorScheme +{ + public SKColor GetColor(int seed) + { + var hash = Math.Abs(seed); + var ratio = (hash % 100) / 100.0f; + + return InterpolateColor(startColor, endColor, ratio); + } + + private static SKColor InterpolateColor(SKColor start, SKColor end, float ratio) + { + var r = (byte)(start.Red + (end.Red - start.Red) * ratio); + var g = (byte)(start.Green + (end.Green - start.Green) * ratio); + var b = (byte)(start.Blue + (end.Blue - start.Blue) * ratio); + + return new SKColor(r, g, b); + } +} \ No newline at end of file diff --git a/TagsCloudContainer.Core/Infrastructure/Coloring/IColorScheme.cs b/TagsCloudContainer.Core/Infrastructure/Coloring/IColorScheme.cs new file mode 100644 index 000000000..a777cd358 --- /dev/null +++ b/TagsCloudContainer.Core/Infrastructure/Coloring/IColorScheme.cs @@ -0,0 +1,7 @@ +using SkiaSharp; + +namespace TagsCloudContainer.Core.Infrastructure.Coloring; +public interface IColorScheme +{ + SKColor GetColor(int seed); +} \ No newline at end of file diff --git a/TagsCloudContainer.Core/Infrastructure/Coloring/RandomColorScheme.cs b/TagsCloudContainer.Core/Infrastructure/Coloring/RandomColorScheme.cs new file mode 100644 index 000000000..e97b27da0 --- /dev/null +++ b/TagsCloudContainer.Core/Infrastructure/Coloring/RandomColorScheme.cs @@ -0,0 +1,22 @@ +using SkiaSharp; + +namespace TagsCloudContainer.Core.Infrastructure.Coloring; +public class RandomColorScheme : IColorScheme +{ + private readonly SKColor[] _colors = + [ + SKColors.Red, SKColors.Blue, SKColors.Green, SKColors.Purple, + SKColors.Orange, new(139, 0, 0), + new(0, 0, 139), + new(0, 100, 0), + new(165, 42, 42), + new(0, 139, 139), + new(139, 0, 139) + ]; + + public SKColor GetColor(int seed) + { + var index = Math.Abs(seed) % _colors.Length; + return _colors[index]; + } +} \ No newline at end of file diff --git a/TagsCloudContainer.Core/Infrastructure/Layout/ArchimedeanSpiralPointGenerator.cs b/TagsCloudContainer.Core/Infrastructure/Layout/ArchimedeanSpiralPointGenerator.cs new file mode 100644 index 000000000..4d9809cee --- /dev/null +++ b/TagsCloudContainer.Core/Infrastructure/Layout/ArchimedeanSpiralPointGenerator.cs @@ -0,0 +1,19 @@ +using SkiaSharp; + +namespace TagsCloudContainer.Core.Infrastructure.Layout; +internal class ArchimedeanSpiralPointGenerator( + SKPoint center, + double angleStep = 0.1, + double radiusStep = 0.5) +{ + private double _angle; + + public SKPoint GetNextPoint() + { + var radius = radiusStep * _angle; + var x = center.X + (float)Math.Round(radius * Math.Cos(_angle)); + var y = center.Y + (float)Math.Round(radius * Math.Sin(_angle)); + _angle += angleStep; + return new SKPoint(x, y); + } +} \ No newline at end of file diff --git a/TagsCloudContainer.Core/Infrastructure/Layout/IFontSizeCalculator.cs b/TagsCloudContainer.Core/Infrastructure/Layout/IFontSizeCalculator.cs new file mode 100644 index 000000000..340c90395 --- /dev/null +++ b/TagsCloudContainer.Core/Infrastructure/Layout/IFontSizeCalculator.cs @@ -0,0 +1,6 @@ +namespace TagsCloudContainer.Core.Infrastructure.Layout; + +public interface IFontSizeCalculator +{ + float Calculate(int frequency, int maxFrequency); +} \ No newline at end of file diff --git a/TagsCloudContainer.Core/Infrastructure/Layout/ITagCloudAlgorithm.cs b/TagsCloudContainer.Core/Infrastructure/Layout/ITagCloudAlgorithm.cs new file mode 100644 index 000000000..a5ced3dc3 --- /dev/null +++ b/TagsCloudContainer.Core/Infrastructure/Layout/ITagCloudAlgorithm.cs @@ -0,0 +1,9 @@ +using TagsCloudContainer.Core.Models; + +namespace TagsCloudContainer.Core.Infrastructure.Layout; +public interface ITagCloudAlgorithm +{ + IEnumerable Arrange( + IEnumerable wordFrequencies, + LayoutOptions options); +} \ No newline at end of file diff --git a/TagsCloudContainer.Core/Infrastructure/Layout/LinearFontSizeCalculator.cs b/TagsCloudContainer.Core/Infrastructure/Layout/LinearFontSizeCalculator.cs new file mode 100644 index 000000000..fa671263b --- /dev/null +++ b/TagsCloudContainer.Core/Infrastructure/Layout/LinearFontSizeCalculator.cs @@ -0,0 +1,15 @@ +namespace TagsCloudContainer.Core.Infrastructure.Layout; +public class LinearFontSizeCalculator(float minFontSize = 10, float maxFontSize = 50) + : IFontSizeCalculator +{ + public float Calculate(int frequency, int maxFrequency) + { + if (maxFrequency <= 0) + return minFontSize; + + var ratio = (float)frequency / maxFrequency; + var fontSize = minFontSize + (maxFontSize - minFontSize) * ratio; + + return Math.Max(minFontSize, Math.Min(maxFontSize, fontSize)); + } +} diff --git a/TagsCloudContainer.Core/Infrastructure/Layout/RectangleCloudLayouter.cs b/TagsCloudContainer.Core/Infrastructure/Layout/RectangleCloudLayouter.cs new file mode 100644 index 000000000..e2e50ceb3 --- /dev/null +++ b/TagsCloudContainer.Core/Infrastructure/Layout/RectangleCloudLayouter.cs @@ -0,0 +1,69 @@ +using SkiaSharp; + +namespace TagsCloudContainer.Core.Infrastructure.Layout; +internal class RectangleCloudLayouter( + SKPoint center, + ArchimedeanSpiralPointGenerator generator) +{ + private readonly List _rectangles = []; + public SKRect PutNextRectangle(SKSize size) + { + if (size.Width <= 0 || size.Height <= 0) + throw new ArgumentException("Rectangle size must be greater than zero"); + + var rect = _rectangles.Count == 0 + ? CreateRect(center, size) + : ShiftToCenter(FindPlace(size)); + + _rectangles.Add(rect); + return rect; + } + + private SKRect FindPlace(SKSize size) + { + while (true) + { + var point = generator.GetNextPoint(); + var candidate = CreateRect(point, size); + if (_rectangles.All(r => !r.IntersectsWith(candidate))) + return candidate; + } + } + + private SKRect ShiftToCenter(SKRect rect) + { + while (true) + { + var direction = GetDirectionToCenter(rect); + if (direction == SKPoint.Empty) + return rect; + + var shifted = rect.OffsetClone(direction.X, direction.Y); + if (_rectangles.Any(r => r.IntersectsWith(shifted))) + return rect; + + rect = shifted; + } + } + + private SKPoint GetDirectionToCenter(SKRect rect) + { + var center1 = new SKPoint((rect.Left + rect.Right) / 2f, (rect.Top + rect.Bottom) / 2f); + var dx = center1.X > center.X ? -1 : center1.X < center.X ? 1 : 0; + var dy = center1.Y > center.Y ? -1 : center1.Y < center.Y ? 1 : 0; + return new SKPoint(dx, dy); + } + + private static SKRect CreateRect(SKPoint center, SKSize size) + { + var left = center.X - size.Width / 2; + var top = center.Y - size.Height / 2; + return new SKRect(left, top, left + size.Width, top + size.Height); + } +} + +internal static class SkRectExtensions +{ + public static SKRect OffsetClone(this SKRect rect, float dx, float dy) => + new(rect.Left + dx, rect.Top + dy, rect.Right + dx, rect.Bottom + dy); +} diff --git a/TagsCloudContainer.Core/Infrastructure/Layout/SpiralTagCloudAlgorithm.cs b/TagsCloudContainer.Core/Infrastructure/Layout/SpiralTagCloudAlgorithm.cs new file mode 100644 index 000000000..c96071cde --- /dev/null +++ b/TagsCloudContainer.Core/Infrastructure/Layout/SpiralTagCloudAlgorithm.cs @@ -0,0 +1,90 @@ +using SkiaSharp; +using TagsCloudContainer.Core.Infrastructure.Coloring; +using TagsCloudContainer.Core.Models; + +namespace TagsCloudContainer.Core.Infrastructure.Layout; +public class SpiralTagCloudAlgorithm( + IFontSizeCalculator fontSizeCalculator, + IColorScheme colorScheme) + : ITagCloudAlgorithm +{ + public IEnumerable Arrange( + IEnumerable wordFrequencies, + LayoutOptions options) + { + var frequencies = wordFrequencies.ToArray(); + if (frequencies.Length == 0) + yield break; + + var maxFrequency = frequencies.Max(wf => wf.Frequency); + var placedRectangles = new List(); + var center = options.Center; + var typeface = SKTypeface.FromFamilyName(options.FontFamily); + + foreach (var wordFrequency in frequencies) + { + var fontSize = fontSizeCalculator.Calculate( + wordFrequency.Frequency, maxFrequency); + + var size = MeasureWordSize(wordFrequency.Word, typeface, fontSize); + var location = FindLocationForRectangle(size, center, placedRectangles); + + if (location == SKPoint.Empty) + continue; + + var rectangle = new SKRect(location.X, location.Y, + location.X + size.Width, location.Y + size.Height); + placedRectangles.Add(rectangle); + + var seed = wordFrequency.Word.GetHashCode(); + var color = colorScheme.GetColor(seed); + + yield return new LayoutWord(wordFrequency, typeface, fontSize, color, rectangle); + } + } + + private static SKSize MeasureWordSize( + string word, + SKTypeface typeface, + float fontSize) + { + using var paint = new SKPaint(); + paint.Typeface = typeface; + paint.TextSize = fontSize; + + var bounds = new SKRect(); + paint.MeasureText(word, ref bounds); + return new SKSize(bounds.Width, bounds.Height); + } + + private static SKPoint FindLocationForRectangle( + SKSize size, + SKPoint center, + List placedRectangles) + { + const double angleStep = 0.1; + const double radiusStep = 1; + + double angle = 0; + double radius = 0; + + for (var i = 0; i < 1000; i++) + { + var x = center.X + (float)(radius * Math.Cos(angle)) - size.Width / 2; + var y = center.Y + (float)(radius * Math.Sin(angle)) - size.Height / 2; + + var candidate = new SKRect(x, y, x + size.Width, y + size.Height); + + if (!placedRectangles.Any(rect => rect.IntersectsWith(candidate)) && + candidate is { Left: >= 0, Top: >= 0 }) + { + return new SKPoint(x, y); + } + angle += angleStep; + radius += radiusStep; + } + + return SKPoint.Empty; + } +} + diff --git a/TagsCloudContainer.Core/Infrastructure/Layout/TagCloudAlgorithmType.cs b/TagsCloudContainer.Core/Infrastructure/Layout/TagCloudAlgorithmType.cs new file mode 100644 index 000000000..99b1a30af --- /dev/null +++ b/TagsCloudContainer.Core/Infrastructure/Layout/TagCloudAlgorithmType.cs @@ -0,0 +1,7 @@ +namespace TagsCloudContainer.Core.Infrastructure.Layout; + +public enum TagCloudAlgorithmType +{ + Spiral, + Tight +} \ No newline at end of file diff --git a/TagsCloudContainer.Core/Infrastructure/Layout/TightSpiralTagCloudAlgorithm.cs b/TagsCloudContainer.Core/Infrastructure/Layout/TightSpiralTagCloudAlgorithm.cs new file mode 100644 index 000000000..f00bb6602 --- /dev/null +++ b/TagsCloudContainer.Core/Infrastructure/Layout/TightSpiralTagCloudAlgorithm.cs @@ -0,0 +1,48 @@ +using SkiaSharp; +using TagsCloudContainer.Core.Infrastructure.Coloring; +using TagsCloudContainer.Core.Models; + +namespace TagsCloudContainer.Core.Infrastructure.Layout; +public class TightSpiralTagCloudAlgorithm( + IFontSizeCalculator fontSizeCalculator, + IColorScheme colorScheme) + : ITagCloudAlgorithm +{ + public IEnumerable Arrange(IEnumerable + wordFrequencies, LayoutOptions options) + { + var frequencies = wordFrequencies.ToArray(); + if (frequencies.Length == 0) + yield break; + + var maxFrequency = frequencies.Max(x => x.Frequency); + var typeface = SKTypeface.FromFamilyName(options.FontFamily); + var layouter = new RectangleCloudLayouter(options.Center, + new ArchimedeanSpiralPointGenerator(options.Center)); + + foreach (var wordFrequency in frequencies) + { + var fontSize = fontSizeCalculator.Calculate(wordFrequency.Frequency, + maxFrequency); + var size = MeasureWordSize(wordFrequency.Word, typeface, fontSize); + var rect = layouter.PutNextRectangle(size); + var seed = wordFrequency.Word.GetHashCode(); + var color = colorScheme.GetColor(seed); + + yield return new LayoutWord(wordFrequency, typeface, fontSize, color, rect); + } + } + + private static SKSize MeasureWordSize( + string word, + SKTypeface typeface, + float fontSize) + { + using var paint = new SKPaint(); + paint.Typeface = typeface; + paint.TextSize = fontSize; + var bounds = new SKRect(); + paint.MeasureText(word, ref bounds); + return new SKSize(bounds.Width, bounds.Height); + } +} diff --git a/TagsCloudContainer.Core/Infrastructure/Preprocessing/BoringWordsFilter.cs b/TagsCloudContainer.Core/Infrastructure/Preprocessing/BoringWordsFilter.cs new file mode 100644 index 000000000..4195a42a3 --- /dev/null +++ b/TagsCloudContainer.Core/Infrastructure/Preprocessing/BoringWordsFilter.cs @@ -0,0 +1,41 @@ +namespace TagsCloudContainer.Core.Infrastructure.Preprocessing; +public class BoringWordsFilter : ITextPreprocessor +{ + private readonly IWordFilter[] _filters; + + public BoringWordsFilter(IEnumerable? boringWords) + { + var boringWords1 = new HashSet( + boringWords ?? DefaultBoringWords, + StringComparer.OrdinalIgnoreCase); + + _filters = + [ + new BoringWordFilter(boringWords1) + ]; + } + + private static readonly string[] DefaultBoringWords = + [ + "и", "в", "не", "на", "я", "он", "с", "что", "а", "по", + "это", "как", "но", "они", "к", "у", "же", "вы", "за", + "бы", "о", "из", "от", "то", "все", "его", "до", "для", + "мы", "при", "так", "та", "их", "чем", "или", "быть", + "вот", "от", "под", "ну", "ни", "да", "ли", "если", "еще" + ]; + + public IReadOnlyList Process(IEnumerable words) + { + return words + .Where(word => _filters.All(filter + => !filter.ShouldExclude(word))).ToArray(); + } + + private class BoringWordFilter(HashSet boringWords) : IWordFilter + { + public bool ShouldExclude(string word) + { + return boringWords.Contains(word); + } + } +} \ No newline at end of file diff --git a/TagsCloudContainer.Core/Infrastructure/Preprocessing/EmptyBoringWordsProvider.cs b/TagsCloudContainer.Core/Infrastructure/Preprocessing/EmptyBoringWordsProvider.cs new file mode 100644 index 000000000..fae981294 --- /dev/null +++ b/TagsCloudContainer.Core/Infrastructure/Preprocessing/EmptyBoringWordsProvider.cs @@ -0,0 +1,6 @@ +namespace TagsCloudContainer.Core.Infrastructure.Preprocessing; + +public class EmptyBoringWordsProvider : IBoringWordsProvider +{ + public string[] GetWords() => []; +} \ No newline at end of file diff --git a/TagsCloudContainer.Core/Infrastructure/Preprocessing/FileBoringWordsProvider.cs b/TagsCloudContainer.Core/Infrastructure/Preprocessing/FileBoringWordsProvider.cs new file mode 100644 index 000000000..6ea4f6174 --- /dev/null +++ b/TagsCloudContainer.Core/Infrastructure/Preprocessing/FileBoringWordsProvider.cs @@ -0,0 +1,6 @@ +namespace TagsCloudContainer.Core.Infrastructure.Preprocessing; + +public class FileBoringWordsProvider(string path) : IBoringWordsProvider +{ + public string[] GetWords() => File.ReadAllLines(path); +} \ No newline at end of file diff --git a/TagsCloudContainer.Core/Infrastructure/Preprocessing/IBoringWordsProvider.cs b/TagsCloudContainer.Core/Infrastructure/Preprocessing/IBoringWordsProvider.cs new file mode 100644 index 000000000..db284ca25 --- /dev/null +++ b/TagsCloudContainer.Core/Infrastructure/Preprocessing/IBoringWordsProvider.cs @@ -0,0 +1,6 @@ +namespace TagsCloudContainer.Core.Infrastructure.Preprocessing; + +public interface IBoringWordsProvider +{ + string[] GetWords(); +} \ No newline at end of file diff --git a/TagsCloudContainer.Core/Infrastructure/Preprocessing/ITextPreprocessor.cs b/TagsCloudContainer.Core/Infrastructure/Preprocessing/ITextPreprocessor.cs new file mode 100644 index 000000000..0ba9b0e87 --- /dev/null +++ b/TagsCloudContainer.Core/Infrastructure/Preprocessing/ITextPreprocessor.cs @@ -0,0 +1,5 @@ +namespace TagsCloudContainer.Core.Infrastructure.Preprocessing; +public interface ITextPreprocessor +{ + IReadOnlyList Process(IEnumerable words); +} \ No newline at end of file diff --git a/TagsCloudContainer.Core/Infrastructure/Preprocessing/IWordFilter.cs b/TagsCloudContainer.Core/Infrastructure/Preprocessing/IWordFilter.cs new file mode 100644 index 000000000..ae9cb5e1b --- /dev/null +++ b/TagsCloudContainer.Core/Infrastructure/Preprocessing/IWordFilter.cs @@ -0,0 +1,5 @@ +namespace TagsCloudContainer.Core.Infrastructure.Preprocessing; +public interface IWordFilter +{ + bool ShouldExclude(string word); +} \ No newline at end of file diff --git a/TagsCloudContainer.Core/Infrastructure/Preprocessing/LowerCaseNormalizer.cs b/TagsCloudContainer.Core/Infrastructure/Preprocessing/LowerCaseNormalizer.cs new file mode 100644 index 000000000..bed82bc6b --- /dev/null +++ b/TagsCloudContainer.Core/Infrastructure/Preprocessing/LowerCaseNormalizer.cs @@ -0,0 +1,8 @@ +namespace TagsCloudContainer.Core.Infrastructure.Preprocessing; +public class LowerCaseNormalizer : ITextPreprocessor +{ + public IReadOnlyList Process(IEnumerable words) + { + return words.Select(word => word.ToLowerInvariant()).ToArray(); + } +} \ No newline at end of file diff --git a/TagsCloudContainer.Core/Infrastructure/Reading/DocxTextReader.cs b/TagsCloudContainer.Core/Infrastructure/Reading/DocxTextReader.cs new file mode 100644 index 000000000..94a8d80cf --- /dev/null +++ b/TagsCloudContainer.Core/Infrastructure/Reading/DocxTextReader.cs @@ -0,0 +1,30 @@ +using DocumentFormat.OpenXml.Packaging; +using DocumentFormat.OpenXml.Wordprocessing; + +namespace TagsCloudContainer.Core.Infrastructure.Reading; +public class DocxTextReader : IFileTextReader +{ + public bool CanRead(string filePath) => + filePath.EndsWith(".docx", StringComparison.OrdinalIgnoreCase); + + public IReadOnlyList ReadWords(string filePath) + { + if (!File.Exists(filePath)) + throw new FileNotFoundException($"File not found: {filePath}"); + + using var doc = WordprocessingDocument.Open(filePath, false); + var body = doc.MainDocumentPart?.Document?.Body; + if (body == null) + return []; + + var words = new List(); + foreach (var line in body.Descendants().Select(t => t.Text)) + { + if (string.IsNullOrWhiteSpace(line)) + continue; + words.AddRange(line.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries)); + } + + return words; + } +} diff --git a/TagsCloudContainer.Core/Infrastructure/Reading/ITextReader.cs b/TagsCloudContainer.Core/Infrastructure/Reading/ITextReader.cs new file mode 100644 index 000000000..aa6521ccc --- /dev/null +++ b/TagsCloudContainer.Core/Infrastructure/Reading/ITextReader.cs @@ -0,0 +1,10 @@ +namespace TagsCloudContainer.Core.Infrastructure.Reading; + +public interface ITextReader +{ + bool CanRead(string filePath); + IReadOnlyList ReadWords(string filePath); +} + +public interface IFileTextReader : ITextReader +{ } \ No newline at end of file diff --git a/TagsCloudContainer.Core/Infrastructure/Reading/MultiFormatTextReader.cs b/TagsCloudContainer.Core/Infrastructure/Reading/MultiFormatTextReader.cs new file mode 100644 index 000000000..c07c3785e --- /dev/null +++ b/TagsCloudContainer.Core/Infrastructure/Reading/MultiFormatTextReader.cs @@ -0,0 +1,16 @@ +namespace TagsCloudContainer.Core.Infrastructure.Reading; +public class MultiFormatTextReader(IEnumerable readers) : ITextReader +{ + private readonly IReadOnlyCollection _readers = readers.ToArray(); + + public bool CanRead(string filePath) => _readers.Any(r => + r.CanRead(filePath)); + + public IReadOnlyList ReadWords(string filePath) + { + var reader = _readers.FirstOrDefault(r => r.CanRead(filePath)); + return reader == null ? + throw new NotSupportedException($"No reader for file {filePath}") + : reader.ReadWords(filePath); + } +} diff --git a/TagsCloudContainer.Core/Infrastructure/Reading/TextFileReader.cs b/TagsCloudContainer.Core/Infrastructure/Reading/TextFileReader.cs new file mode 100644 index 000000000..ad98bfa4a --- /dev/null +++ b/TagsCloudContainer.Core/Infrastructure/Reading/TextFileReader.cs @@ -0,0 +1,18 @@ +namespace TagsCloudContainer.Core.Infrastructure.Reading; +public class TextFileReader : IFileTextReader +{ + public bool CanRead(string filePath) + { + return filePath.EndsWith(".txt", StringComparison.OrdinalIgnoreCase); + } + + public IReadOnlyList ReadWords(string filePath) + { + if (!File.Exists(filePath)) + throw new FileNotFoundException($"File not found: {filePath}"); + + return File.ReadLines(filePath) + .Where(line => !string.IsNullOrWhiteSpace(line)) + .Select(line => line.Trim()).ToArray(); + } +} \ No newline at end of file diff --git a/TagsCloudContainer.Core/Infrastructure/Rendering/ITagCloudRenderer.cs b/TagsCloudContainer.Core/Infrastructure/Rendering/ITagCloudRenderer.cs new file mode 100644 index 000000000..e1fb0fbe0 --- /dev/null +++ b/TagsCloudContainer.Core/Infrastructure/Rendering/ITagCloudRenderer.cs @@ -0,0 +1,9 @@ +using SkiaSharp; +using TagsCloudContainer.Core.Models; + +namespace TagsCloudContainer.Core.Infrastructure.Rendering; +public interface ITagCloudRenderer +{ + SKImage Render(IEnumerable layoutWords, LayoutOptions options); + void SaveToFile(SKImage image, string filePath); +} \ No newline at end of file diff --git a/TagsCloudContainer.Core/Infrastructure/Rendering/PngRenderer.cs b/TagsCloudContainer.Core/Infrastructure/Rendering/PngRenderer.cs new file mode 100644 index 000000000..7a606dd1c --- /dev/null +++ b/TagsCloudContainer.Core/Infrastructure/Rendering/PngRenderer.cs @@ -0,0 +1,38 @@ +using SkiaSharp; +using TagsCloudContainer.Core.Models; + +namespace TagsCloudContainer.Core.Infrastructure.Rendering; +public class PngRenderer : ITagCloudRenderer +{ + public SKImage Render(IEnumerable layoutWords, LayoutOptions options) + { + var info = new SKImageInfo(options.Width, options.Height); + using var surface = SKSurface.Create(info); + var canvas = surface.Canvas; + + canvas.Clear(options.BackgroundColor); + + foreach (var layoutWord in layoutWords) + { + using var paint = new SKPaint(); + paint.Color = layoutWord.Color; + paint.Typeface = layoutWord.Typeface; + paint.TextSize = layoutWord.FontSize; + paint.IsAntialias = true; + + canvas.DrawText( + layoutWord.WordFrequency.Word, + layoutWord.Rectangle.Left, + layoutWord.Rectangle.Bottom, + paint); + } + return surface.Snapshot(); + } + + public void SaveToFile(SKImage image, string filePath) + { + using var data = image.Encode(SKEncodedImageFormat.Png, 100); + using var stream = File.OpenWrite(filePath); + data.SaveTo(stream); + } +} \ No newline at end of file diff --git a/TagsCloudContainer.Core/Models/LayoutOptions.cs b/TagsCloudContainer.Core/Models/LayoutOptions.cs new file mode 100644 index 000000000..0521184cb --- /dev/null +++ b/TagsCloudContainer.Core/Models/LayoutOptions.cs @@ -0,0 +1,16 @@ +using SkiaSharp; + +namespace TagsCloudContainer.Core.Models; +public class LayoutOptions +{ + public int Width { get; init; } = 800; + public int Height { get; init; } = 600; + public string FontFamily { get; init; } = "Arial"; + public SKColor BackgroundColor { get; } = SKColors.White; + public SKPoint Center { get; } + + public LayoutOptions() + { + Center = new SKPoint(Width / 2f, Height / 2f); + } +} \ No newline at end of file diff --git a/TagsCloudContainer.Core/Models/LayoutWord.cs b/TagsCloudContainer.Core/Models/LayoutWord.cs new file mode 100644 index 000000000..e0850243d --- /dev/null +++ b/TagsCloudContainer.Core/Models/LayoutWord.cs @@ -0,0 +1,21 @@ +using SkiaSharp; + +namespace TagsCloudContainer.Core.Models; +public class LayoutWord +{ + public WordFrequency WordFrequency { get; } + public SKTypeface Typeface { get; } + public float FontSize { get; } + public SKColor Color { get; } + public SKRect Rectangle { get; } + + public LayoutWord(WordFrequency wordFrequency, SKTypeface typeface, + float fontSize, SKColor color, SKRect rectangle) + { + WordFrequency = wordFrequency; + Typeface = typeface; + FontSize = fontSize; + Color = color; + Rectangle = rectangle; + } +} \ No newline at end of file diff --git a/TagsCloudContainer.Core/Models/TagCloudSettings.cs b/TagsCloudContainer.Core/Models/TagCloudSettings.cs new file mode 100644 index 000000000..a8a9e668d --- /dev/null +++ b/TagsCloudContainer.Core/Models/TagCloudSettings.cs @@ -0,0 +1,15 @@ +using TagsCloudContainer.Core.Infrastructure.Coloring; +using TagsCloudContainer.Core.Infrastructure.Layout; + +namespace TagsCloudContainer.Core.Models; +public class TagCloudSettings +{ + public int Width { get; set; } = 800; + public int Height { get; set; } = 600; + public string FontFamily { get; set; } = "Arial"; + public ColorSchemeType ColorScheme { get; init; } = ColorSchemeType.Random; + public TagCloudAlgorithmType Algorithm { get; init; } = TagCloudAlgorithmType.Spiral; + public int MinFontSize { get; init; } = 10; + public int MaxFontSize { get; init; } = 50; + public string? BoringWordsPath { get; init; } = string.Empty; +} diff --git a/TagsCloudContainer.Core/Models/WordFrequency.cs b/TagsCloudContainer.Core/Models/WordFrequency.cs new file mode 100644 index 000000000..df02e7b76 --- /dev/null +++ b/TagsCloudContainer.Core/Models/WordFrequency.cs @@ -0,0 +1,13 @@ +namespace TagsCloudContainer.Core.Models; +public class WordFrequency +{ + public string Word { get; } + public int Frequency { get; } + + public WordFrequency(string word, int frequency) + { + ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(frequency, 0); + Word = word; + Frequency = frequency; + } +} \ No newline at end of file diff --git a/TagsCloudContainer.Core/TagsCloudContainer.Core.csproj b/TagsCloudContainer.Core/TagsCloudContainer.Core.csproj new file mode 100644 index 000000000..1e3ec05d4 --- /dev/null +++ b/TagsCloudContainer.Core/TagsCloudContainer.Core.csproj @@ -0,0 +1,18 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + diff --git a/TagsCloudContainer.Tests/PngRendererTests.cs b/TagsCloudContainer.Tests/PngRendererTests.cs new file mode 100644 index 000000000..0b4d5e795 --- /dev/null +++ b/TagsCloudContainer.Tests/PngRendererTests.cs @@ -0,0 +1,44 @@ +using System.IO; +using FluentAssertions; +using NUnit.Framework; +using SkiaSharp; +using TagsCloudContainer.Core.Infrastructure.Rendering; +using TagsCloudContainer.Core.Models; + +namespace TagsCloudContainer.Tests; + +[TestFixture] +public class PngRendererTests +{ + [Test] + public void SaveToFile_ShouldCreateNonEmptyPng() + { + var renderer = new PngRenderer(); + var options = new LayoutOptions(); + var typeface = SKTypeface.FromFamilyName(options.FontFamily); + + var layoutWords = new[] + { + new LayoutWord(new WordFrequency("hello", 1), typeface, 24, + SKColors.Black, new SKRect(10, 10, 70, 40)), + new LayoutWord(new WordFrequency("world", 1), typeface, 24, + SKColors.Blue, new SKRect(80, 10, 150, 40)) + }; + + using var image = renderer.Render(layoutWords, options); + var tempPath = Path.GetTempFileName(); + + try + { + renderer.SaveToFile(image, tempPath); + + File.Exists(tempPath).Should().BeTrue(); + new FileInfo(tempPath).Length.Should().BeGreaterThan(0); + } + finally + { + if (File.Exists(tempPath)) + File.Delete(tempPath); + } + } +} diff --git a/TagsCloudContainer.Tests/SpiralTagCloudAlgorithmTests.cs b/TagsCloudContainer.Tests/SpiralTagCloudAlgorithmTests.cs new file mode 100644 index 000000000..5a0b2316d --- /dev/null +++ b/TagsCloudContainer.Tests/SpiralTagCloudAlgorithmTests.cs @@ -0,0 +1,68 @@ +using System.Linq; +using FluentAssertions; +using NUnit.Framework; +using NSubstitute; +using SkiaSharp; +using TagsCloudContainer.Core.Infrastructure.Coloring; +using TagsCloudContainer.Core.Infrastructure.Layout; +using TagsCloudContainer.Core.Models; + +namespace TagsCloudContainer.Tests; + +[TestFixture] +public class SpiralTagCloudAlgorithmTests +{ + [Test] + public void Arrange_ShouldPlaceAllWordsWithoutIntersections() + { + const float fontSize = 20f; + var fontCalculator = Substitute.For(); + fontCalculator.Calculate(Arg.Any(), Arg.Any()).Returns(fontSize); + + var colorScheme = Substitute.For(); + colorScheme.GetColor(Arg.Any()).Returns(SKColors.Black); + + var algorithm = new SpiralTagCloudAlgorithm(fontCalculator, colorScheme); + var options = new LayoutOptions(); + + var frequencies = new[] + { + new WordFrequency("one", 5), + new WordFrequency("two", 3), + new WordFrequency("three", 1) + }; + + var layoutWords = algorithm.Arrange(frequencies, options).ToArray(); + + layoutWords.Should().HaveCount(frequencies.Length); + + for (var i = 0; i < layoutWords.Length; i++) + { + for (var j = i + 1; j < layoutWords.Length; j++) + { + layoutWords[i].Rectangle.IntersectsWith(layoutWords[j].Rectangle) + .Should().BeFalse($"rectangles {i} and {j} should not overlap"); + } + } + } + + [Test] + public void Arrange_ShouldPlaceFirstWordAroundCenter() + { + var fontCalculator = Substitute.For(); + fontCalculator.Calculate(Arg.Any(), Arg.Any()).Returns(18f); + + var colorScheme = Substitute.For(); + colorScheme.GetColor(Arg.Any()).Returns(SKColors.Black); + + var algorithm = new SpiralTagCloudAlgorithm(fontCalculator, colorScheme); + var options = new LayoutOptions(); + + var frequencies = new[] { new WordFrequency("center", 10) }; + + var layoutWords = algorithm.Arrange(frequencies, options).ToArray(); + + layoutWords.Should().ContainSingle(); + layoutWords[0].Rectangle.Contains(options.Center).Should().BeTrue(); + } +} diff --git a/TagsCloudContainer.Tests/TagsCloudContainer.Tests.csproj b/TagsCloudContainer.Tests/TagsCloudContainer.Tests.csproj new file mode 100644 index 000000000..b4ce3435e --- /dev/null +++ b/TagsCloudContainer.Tests/TagsCloudContainer.Tests.csproj @@ -0,0 +1,20 @@ + + + net8.0 + enable + false + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/TagsCloudContainer.Tests/TextFileReaderTests.cs b/TagsCloudContainer.Tests/TextFileReaderTests.cs new file mode 100644 index 000000000..5c1665872 --- /dev/null +++ b/TagsCloudContainer.Tests/TextFileReaderTests.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using FluentAssertions; +using NUnit.Framework; +using TagsCloudContainer.Core.Infrastructure.Reading; + +namespace TagsCloudContainer.Tests; +[TestFixture] +public class TextFileReaderTests +{ + private TextFileReader _reader; + + [SetUp] + public void SetUp() => _reader = new TextFileReader(); + + [Test] + public void CanRead_ShouldReturnTrue_ForTxtFiles() + { + _reader.CanRead("test.txt").Should().BeTrue(); + } + + [Test] + public void CanRead_ShouldReturnFalse_ForNonTxtFiles() + { + _reader.CanRead("test.doc").Should().BeFalse(); + } + + [Test] + public void ReadWords_ShouldReturnAllLines_FromExistingFile() + { + using var temp = new TempFile(new[] { "word1", "word2", "word3" }); + + var words = _reader.ReadWords(temp.Path).ToArray(); + + words.Should().Equal("word1", "word2", "word3"); + } + + [Test] + public void + ReadWords_ShouldThrowFileNotFoundException_ForNonExistingFile() + { + _reader.Invoking(r => r.ReadWords("non_existing_file.txt").ToArray()) + .Should().Throw(); + } + + [Test] + public void ReadWords_ShouldSkipEmptyLines() + { + using var temp = new TempFile(new[] { "word1", "", "word2", " ", + "word3" }); + + var words = _reader.ReadWords(temp.Path).ToArray(); + + words.Should().Equal("word1", "word2", "word3"); + } + + private sealed class TempFile : IDisposable + { + public string Path { get; } + + public TempFile(IEnumerable lines) + { + Path = System.IO.Path.GetTempFileName(); + File.WriteAllLines(Path, lines); + } + public void Dispose() + { + if (File.Exists(Path)) + File.Delete(Path); + } + } +} diff --git a/TagsCloudContainer.Tests/words.txt b/TagsCloudContainer.Tests/words.txt new file mode 100644 index 000000000..266b769b0 --- /dev/null +++ b/TagsCloudContainer.Tests/words.txt @@ -0,0 +1,106 @@ +программирование +программирование +программирование +программирование +программирование +код +код +код +код +алгоритм +алгоритм +алгоритм +тестирование +тестирование +разработка +разработка +система +система +данные +данные +база +база +приложение +приложение +интерфейс +интерфейс +пользователь +пользователь +операционный +компилятор +интерпретатор +функция +метод +класс +объект +наследование +полиморфизм +инкапсуляция +абстракция +библиотека +фреймворк +платформа +сервер +клиент +сеть +протокол +безопасность +шифрование +аутентификация +авторизация +базаданных +запрос +транзакция +индекс +оптимизация +производительность +отладка +деплоймент +контейнеризация +оркестрация +микросервис +монолит +репозиторий +ветка +мерж +конфликт +рефакторинг +документация +комментарий +синтаксис +семантика +память +процессор +жесткий +диск +оперативная +видеокарта +материнская +плата +периферийный +устройство +клавиатура +мышь +монитор +принтер +сканер +наушники +колонки +вебкамера +микрофон +роутер +модем +маршрутизатор +коммутатор +файрвол +антивирус +малвар +вирус +троян +шпионское +по +рекламное +вымогатель +ботнет +фишинг +спам \ No newline at end of file diff --git a/fp.sln b/fp.sln index d104ab530..16ea7b249 100644 --- a/fp.sln +++ b/fp.sln @@ -10,6 +10,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Summator", "Samples\Summato EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConwaysGameOfLife", "Samples\ConwaysGameOfLife\ConwaysGameOfLife.csproj", "{4B77EC28-5FB5-4095-B3D7-127F5C488D6E}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TagsCloudContainer.Console", "TagsCloudContainer.Console\TagsCloudContainer.Console.csproj", "{FFDF4921-C18B-41CD-9505-C1DEB93FC8B8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TagsCloudContainer.Core", "TagsCloudContainer.Core\TagsCloudContainer.Core.csproj", "{E4C37F22-CA7E-4C81-A920-79100D2975AE}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TagsCloudContainer.Tests", "TagsCloudContainer.Tests\TagsCloudContainer.Tests.csproj", "{FFED1ADE-E30A-4352-A206-2B4DA3396F02}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -32,6 +38,18 @@ Global {4B77EC28-5FB5-4095-B3D7-127F5C488D6E}.Debug|Any CPU.Build.0 = Debug|Any CPU {4B77EC28-5FB5-4095-B3D7-127F5C488D6E}.Release|Any CPU.ActiveCfg = Release|Any CPU {4B77EC28-5FB5-4095-B3D7-127F5C488D6E}.Release|Any CPU.Build.0 = Release|Any CPU + {FFDF4921-C18B-41CD-9505-C1DEB93FC8B8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FFDF4921-C18B-41CD-9505-C1DEB93FC8B8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FFDF4921-C18B-41CD-9505-C1DEB93FC8B8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FFDF4921-C18B-41CD-9505-C1DEB93FC8B8}.Release|Any CPU.Build.0 = Release|Any CPU + {E4C37F22-CA7E-4C81-A920-79100D2975AE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E4C37F22-CA7E-4C81-A920-79100D2975AE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E4C37F22-CA7E-4C81-A920-79100D2975AE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E4C37F22-CA7E-4C81-A920-79100D2975AE}.Release|Any CPU.Build.0 = Release|Any CPU + {FFED1ADE-E30A-4352-A206-2B4DA3396F02}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FFED1ADE-E30A-4352-A206-2B4DA3396F02}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FFED1ADE-E30A-4352-A206-2B4DA3396F02}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FFED1ADE-E30A-4352-A206-2B4DA3396F02}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {C33F3A5E-A1ED-4657-9B35-968A4CB23AA1} = {754C1CC8-A8B6-46C6-B35C-8A43B80111A0} diff --git a/out.png b/out.png new file mode 100644 index 0000000000000000000000000000000000000000..f68c7259f3ada54f5e7adb88993ec8cd4cc5f494 GIT binary patch literal 140748 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 Dey>Xx literal 0 HcmV?d00001 diff --git a/words.txt b/words.txt new file mode 100644 index 000000000..266b769b0 --- /dev/null +++ b/words.txt @@ -0,0 +1,106 @@ +программирование +программирование +программирование +программирование +программирование +код +код +код +код +алгоритм +алгоритм +алгоритм +тестирование +тестирование +разработка +разработка +система +система +данные +данные +база +база +приложение +приложение +интерфейс +интерфейс +пользователь +пользователь +операционный +компилятор +интерпретатор +функция +метод +класс +объект +наследование +полиморфизм +инкапсуляция +абстракция +библиотека +фреймворк +платформа +сервер +клиент +сеть +протокол +безопасность +шифрование +аутентификация +авторизация +базаданных +запрос +транзакция +индекс +оптимизация +производительность +отладка +деплоймент +контейнеризация +оркестрация +микросервис +монолит +репозиторий +ветка +мерж +конфликт +рефакторинг +документация +комментарий +синтаксис +семантика +память +процессор +жесткий +диск +оперативная +видеокарта +материнская +плата +периферийный +устройство +клавиатура +мышь +монитор +принтер +сканер +наушники +колонки +вебкамера +микрофон +роутер +модем +маршрутизатор +коммутатор +файрвол +антивирус +малвар +вирус +троян +шпионское +по +рекламное +вымогатель +ботнет +фишинг +спам \ No newline at end of file From e00df84862178471758be8d8deca84bda2bbe9e1 Mon Sep 17 00:00:00 2001 From: Krotkaya Date: Fri, 16 Jan 2026 19:53:45 +0500 Subject: [PATCH 2/6] =?UTF-8?q?=D0=92=D0=BD=D0=B5=D0=B4=D1=80=D0=B8=D0=BB?= =?UTF-8?q?=D0=B0=20=D0=BF=D0=B0=D1=82=D1=82=D0=B5=D1=80=D0=BD=20Result?= =?UTF-8?q?=20=20=D0=B8=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB=D0=B0?= =?UTF-8?q?=20=D1=8F=D0=B2=D0=BD=D1=8B=D0=B5=20=D0=BE=D1=88=D0=B8=D0=B1?= =?UTF-8?q?=D0=BA=D0=B8=20=D1=81=20=D1=81=D0=BE=D0=BE=D0=B1=D1=89=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D1=8F=D0=BC=D0=B8=20=D0=BD=D0=B0=20=D1=83=D1=80?= =?UTF-8?q?=D0=BE=D0=B2=D0=BD=D0=B5=20Core?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- TagsCloudContainer.Core/DI/AutofacModule.cs | 4 +- .../DI/CompositePreprocessor.cs | 10 +- .../Analysis/IWordFrequencyAnalyzer.cs | 3 +- .../Analysis/WordFrequencyAnalyzer.cs | 5 +- .../Layout/ITagCloudAlgorithm.cs | 3 +- .../Layout/RectangleCloudLayouter.cs | 48 +++++-- .../Layout/SpiralTagCloudAlgorithm.cs | 55 ++++---- .../Layout/TightSpiralTagCloudAlgorithm.cs | 36 +++-- .../Preprocessing/BoringWordsFilter.cs | 41 ++---- .../DefaultBoringWordsProvider.cs | 16 +++ .../Preprocessing/EmptyBoringWordsProvider.cs | 3 +- .../Preprocessing/FileBoringWordsProvider.cs | 14 +- .../Preprocessing/IBoringWordsProvider.cs | 4 +- .../Preprocessing/ITextPreprocessor.cs | 3 +- .../Preprocessing/LowerCaseNormalizer.cs | 8 +- .../Infrastructure/Reading/DocxTextReader.cs | 34 ++--- .../Infrastructure/Reading/ITextReader.cs | 3 +- .../Reading/MultiFormatTextReader.cs | 7 +- .../Infrastructure/Reading/TextFileReader.cs | 13 +- .../Rendering/ITagCloudRenderer.cs | 5 +- .../Infrastructure/Rendering/PngRenderer.cs | 63 +++++---- .../Models/LayoutOptions.cs | 7 +- TagsCloudContainer.Core/Result.cs | 123 ++++++++++++++++++ 23 files changed, 358 insertions(+), 150 deletions(-) create mode 100644 TagsCloudContainer.Core/Infrastructure/Preprocessing/DefaultBoringWordsProvider.cs create mode 100644 TagsCloudContainer.Core/Result.cs diff --git a/TagsCloudContainer.Core/DI/AutofacModule.cs b/TagsCloudContainer.Core/DI/AutofacModule.cs index d7c2015a3..f25c4e651 100644 --- a/TagsCloudContainer.Core/DI/AutofacModule.cs +++ b/TagsCloudContainer.Core/DI/AutofacModule.cs @@ -23,13 +23,13 @@ protected override void Load(ContainerBuilder builder) builder.Register(_ => string.IsNullOrWhiteSpace(settings.BoringWordsPath) - ? new EmptyBoringWordsProvider() + ? new DefaultBoringWordsProvider() : new FileBoringWordsProvider(settings.BoringWordsPath)) .SingleInstance(); builder.Register(c => { - var boringWords = c.Resolve().GetWords(); + var boringWords = c.Resolve(); return new CompositePreprocessor([ new LowerCaseNormalizer(), new BoringWordsFilter(boringWords) diff --git a/TagsCloudContainer.Core/DI/CompositePreprocessor.cs b/TagsCloudContainer.Core/DI/CompositePreprocessor.cs index 9ae307aca..b546840cf 100644 --- a/TagsCloudContainer.Core/DI/CompositePreprocessor.cs +++ b/TagsCloudContainer.Core/DI/CompositePreprocessor.cs @@ -1,11 +1,15 @@ +using ResultOf; using TagsCloudContainer.Core.Infrastructure.Preprocessing; namespace TagsCloudContainer.Core.DI; public class CompositePreprocessor(IEnumerable preprocessors) : ITextPreprocessor { - public IReadOnlyList Process(IEnumerable words) + public Result> Process(IEnumerable words) { - return preprocessors.Aggregate(words, (current, preprocessor) - => preprocessor.Process(current)).ToArray(); + var current = Result.Ok>(words.ToArray()); + return preprocessors.Aggregate(current, (current1, preprocessor) + => current1.Then(preprocessor.Process)); } + + } \ No newline at end of file diff --git a/TagsCloudContainer.Core/Infrastructure/Analysis/IWordFrequencyAnalyzer.cs b/TagsCloudContainer.Core/Infrastructure/Analysis/IWordFrequencyAnalyzer.cs index 136994403..4ccd08c42 100644 --- a/TagsCloudContainer.Core/Infrastructure/Analysis/IWordFrequencyAnalyzer.cs +++ b/TagsCloudContainer.Core/Infrastructure/Analysis/IWordFrequencyAnalyzer.cs @@ -1,8 +1,9 @@ +using ResultOf; using TagsCloudContainer.Core.Models; namespace TagsCloudContainer.Core.Infrastructure.Analysis; public interface IWordFrequencyAnalyzer { - IReadOnlyList Analyze(IEnumerable words); + Result> Analyze(IEnumerable words); } \ No newline at end of file diff --git a/TagsCloudContainer.Core/Infrastructure/Analysis/WordFrequencyAnalyzer.cs b/TagsCloudContainer.Core/Infrastructure/Analysis/WordFrequencyAnalyzer.cs index 6fd96e5f0..4538ad644 100644 --- a/TagsCloudContainer.Core/Infrastructure/Analysis/WordFrequencyAnalyzer.cs +++ b/TagsCloudContainer.Core/Infrastructure/Analysis/WordFrequencyAnalyzer.cs @@ -1,15 +1,16 @@ +using ResultOf; using TagsCloudContainer.Core.Models; namespace TagsCloudContainer.Core.Infrastructure.Analysis; public class WordFrequencyAnalyzer : IWordFrequencyAnalyzer { - public IReadOnlyList Analyze(IEnumerable words) + public Result> Analyze(IEnumerable words) { var frequencies = words .GroupBy(word => word) .Select(group => new WordFrequency(group.Key, group.Count())) .OrderByDescending(wf => wf.Frequency).ToArray(); - return frequencies; + return Result.Ok>(frequencies); } } \ No newline at end of file diff --git a/TagsCloudContainer.Core/Infrastructure/Layout/ITagCloudAlgorithm.cs b/TagsCloudContainer.Core/Infrastructure/Layout/ITagCloudAlgorithm.cs index a5ced3dc3..9151f2573 100644 --- a/TagsCloudContainer.Core/Infrastructure/Layout/ITagCloudAlgorithm.cs +++ b/TagsCloudContainer.Core/Infrastructure/Layout/ITagCloudAlgorithm.cs @@ -1,9 +1,10 @@ +using ResultOf; using TagsCloudContainer.Core.Models; namespace TagsCloudContainer.Core.Infrastructure.Layout; public interface ITagCloudAlgorithm { - IEnumerable Arrange( + Result> Arrange( IEnumerable wordFrequencies, LayoutOptions options); } \ No newline at end of file diff --git a/TagsCloudContainer.Core/Infrastructure/Layout/RectangleCloudLayouter.cs b/TagsCloudContainer.Core/Infrastructure/Layout/RectangleCloudLayouter.cs index e2e50ceb3..f509c4588 100644 --- a/TagsCloudContainer.Core/Infrastructure/Layout/RectangleCloudLayouter.cs +++ b/TagsCloudContainer.Core/Infrastructure/Layout/RectangleCloudLayouter.cs @@ -1,3 +1,4 @@ +using ResultOf; using SkiaSharp; namespace TagsCloudContainer.Core.Infrastructure.Layout; @@ -5,31 +6,44 @@ internal class RectangleCloudLayouter( SKPoint center, ArchimedeanSpiralPointGenerator generator) { - private readonly List _rectangles = []; - public SKRect PutNextRectangle(SKSize size) + private readonly List rectangles = []; + public Result PutNextRectangle(SKSize size, SKRect bounds, int maxAttempts = 10000) { if (size.Width <= 0 || size.Height <= 0) - throw new ArgumentException("Rectangle size must be greater than zero"); + return Result.Fail("Rectangle size must be greater than zero"); - var rect = _rectangles.Count == 0 - ? CreateRect(center, size) - : ShiftToCenter(FindPlace(size)); + if (rectangles.Count != 0) + return FindPlace(size, bounds, maxAttempts).Then(rect => + { + var shifted = ShiftToCenter(rect); + if (!IsInside(shifted, bounds)) + return Result.Fail("Tag cloud does not fit the image"); + rectangles.Add(shifted); + return Result.Ok(shifted); + }); + var first = CreateRect(center, size); + if (!IsInside(first, bounds)) + return Result.Fail("Tag cloud does not fit the image"); + rectangles.Add(first); + return Result.Ok(first); - _rectangles.Add(rect); - return rect; } - private SKRect FindPlace(SKSize size) + private Result FindPlace(SKSize size, SKRect bounds, int + maxAttempts) { - while (true) + for (var i = 0; i < maxAttempts; i++) { var point = generator.GetNextPoint(); var candidate = CreateRect(point, size); - if (_rectangles.All(r => !r.IntersectsWith(candidate))) - return candidate; + if (rectangles.All(r => !r.IntersectsWith(candidate)) && + IsInside(candidate, bounds)) + return Result.Ok(candidate); } + return Result.Fail("Tag cloud does not fit the image"); } + private SKRect ShiftToCenter(SKRect rect) { while (true) @@ -39,12 +53,20 @@ private SKRect ShiftToCenter(SKRect rect) return rect; var shifted = rect.OffsetClone(direction.X, direction.Y); - if (_rectangles.Any(r => r.IntersectsWith(shifted))) + if (rectangles.Any(r => r.IntersectsWith(shifted))) return rect; rect = shifted; } } + + private static bool IsInside(SKRect rect, SKRect bounds) + { + return rect.Left >= bounds.Left && + rect.Top >= bounds.Top && + rect.Right <= bounds.Right && + rect.Bottom <= bounds.Bottom; + } private SKPoint GetDirectionToCenter(SKRect rect) { diff --git a/TagsCloudContainer.Core/Infrastructure/Layout/SpiralTagCloudAlgorithm.cs b/TagsCloudContainer.Core/Infrastructure/Layout/SpiralTagCloudAlgorithm.cs index c96071cde..ea2ceec8f 100644 --- a/TagsCloudContainer.Core/Infrastructure/Layout/SpiralTagCloudAlgorithm.cs +++ b/TagsCloudContainer.Core/Infrastructure/Layout/SpiralTagCloudAlgorithm.cs @@ -1,3 +1,4 @@ +using ResultOf; using SkiaSharp; using TagsCloudContainer.Core.Infrastructure.Coloring; using TagsCloudContainer.Core.Models; @@ -8,39 +9,45 @@ public class SpiralTagCloudAlgorithm( IColorScheme colorScheme) : ITagCloudAlgorithm { - public IEnumerable Arrange( + public Result> Arrange( IEnumerable wordFrequencies, LayoutOptions options) { var frequencies = wordFrequencies.ToArray(); if (frequencies.Length == 0) - yield break; + return + Result.Ok>(Array.Empty()); + + var typeface = SKFontManager.Default.MatchFamily(options.FontFamily); + if (typeface == null) + return Result.Fail>($"Font'{options.FontFamily}' not found."); var maxFrequency = frequencies.Max(wf => wf.Frequency); var placedRectangles = new List(); - var center = options.Center; - var typeface = SKTypeface.FromFamilyName(options.FontFamily); + var layoutWords = new List(); + var bounds = new SKRect(0, 0, options.Width, options.Height); foreach (var wordFrequency in frequencies) { var fontSize = fontSizeCalculator.Calculate( wordFrequency.Frequency, maxFrequency); - + var size = MeasureWordSize(wordFrequency.Word, typeface, fontSize); - var location = FindLocationForRectangle(size, center, placedRectangles); - + var location = FindLocationForRectangle(size, options, bounds, placedRectangles); + if (location == SKPoint.Empty) - continue; + return Result.Fail>( + "Tag cloud doesn't fit the image"); var rectangle = new SKRect(location.X, location.Y, location.X + size.Width, location.Y + size.Height); placedRectangles.Add(rectangle); - var seed = wordFrequency.Word.GetHashCode(); - var color = colorScheme.GetColor(seed); - - yield return new LayoutWord(wordFrequency, typeface, fontSize, color, rectangle); + var color = colorScheme.GetColor(wordFrequency.Word.GetHashCode()); + layoutWords.Add(new LayoutWord(wordFrequency, typeface, fontSize, color, rectangle)); } + + return Result.Ok>(layoutWords); } private static SKSize MeasureWordSize( @@ -56,30 +63,34 @@ private static SKSize MeasureWordSize( paint.MeasureText(word, ref bounds); return new SKSize(bounds.Width, bounds.Height); } - + private static SKPoint FindLocationForRectangle( - SKSize size, - SKPoint center, + SKSize size, + LayoutOptions options, + SKRect bounds, List placedRectangles) { const double angleStep = 0.1; const double radiusStep = 1; - double angle = 0; double radius = 0; for (var i = 0; i < 1000; i++) { - var x = center.X + (float)(radius * Math.Cos(angle)) - size.Width / 2; - var y = center.Y + (float)(radius * Math.Sin(angle)) - size.Height / 2; + var x = options.Center.X + (float)(radius * Math.Cos(angle)) - size.Width / 2; + var y = options.Center.Y + (float)(radius * Math.Sin(angle)) - size.Height / 2; var candidate = new SKRect(x, y, x + size.Width, y + size.Height); - if (!placedRectangles.Any(rect => rect.IntersectsWith(candidate)) && - candidate is { Left: >= 0, Top: >= 0 }) - { + var fits = candidate.Left >= bounds.Left && + candidate.Top >= bounds.Top && + candidate.Right <= bounds.Right && + candidate.Bottom <= bounds.Bottom; + + if (fits && !placedRectangles.Any(rect => + rect.IntersectsWith(candidate))) return new SKPoint(x, y); - } + angle += angleStep; radius += radiusStep; } diff --git a/TagsCloudContainer.Core/Infrastructure/Layout/TightSpiralTagCloudAlgorithm.cs b/TagsCloudContainer.Core/Infrastructure/Layout/TightSpiralTagCloudAlgorithm.cs index f00bb6602..4018b1164 100644 --- a/TagsCloudContainer.Core/Infrastructure/Layout/TightSpiralTagCloudAlgorithm.cs +++ b/TagsCloudContainer.Core/Infrastructure/Layout/TightSpiralTagCloudAlgorithm.cs @@ -1,3 +1,4 @@ +using ResultOf; using SkiaSharp; using TagsCloudContainer.Core.Infrastructure.Coloring; using TagsCloudContainer.Core.Models; @@ -8,31 +9,44 @@ public class TightSpiralTagCloudAlgorithm( IColorScheme colorScheme) : ITagCloudAlgorithm { - public IEnumerable Arrange(IEnumerable + public Result> Arrange(IEnumerable wordFrequencies, LayoutOptions options) { var frequencies = wordFrequencies.ToArray(); if (frequencies.Length == 0) - yield break; + return Result.Ok>([]); - var maxFrequency = frequencies.Max(x => x.Frequency); - var typeface = SKTypeface.FromFamilyName(options.FontFamily); - var layouter = new RectangleCloudLayouter(options.Center, + var typeface = SKFontManager.Default.MatchFamily(options.FontFamily); + if (typeface == null) + return Result.Fail>($"Font'{options.FontFamily}' not found"); + + var bounds = new SKRect(0, 0, options.Width, options.Height); + var layouter = new RectangleCloudLayouter(options.Center, new ArchimedeanSpiralPointGenerator(options.Center)); + var maxFrequency = frequencies.Max(x => x.Frequency); + var layoutWords = new List(); + foreach (var wordFrequency in frequencies) { - var fontSize = fontSizeCalculator.Calculate(wordFrequency.Frequency, + var fontSize = fontSizeCalculator.Calculate(wordFrequency.Frequency, maxFrequency); var size = MeasureWordSize(wordFrequency.Word, typeface, fontSize); - var rect = layouter.PutNextRectangle(size); - var seed = wordFrequency.Word.GetHashCode(); - var color = colorScheme.GetColor(seed); + var rectResult = layouter.PutNextRectangle(size, bounds); + if (!rectResult.IsSuccess) + return Result.Fail>(rectResult.Error); - yield return new LayoutWord(wordFrequency, typeface, fontSize, color, rect); + var rect = rectResult.GetValueOrThrow(); + var color = + colorScheme.GetColor(wordFrequency.Word.GetHashCode()); + + layoutWords.Add(new LayoutWord(wordFrequency, typeface, fontSize, + color, rect)); } - } + return Result.Ok>(layoutWords); + } + private static SKSize MeasureWordSize( string word, SKTypeface typeface, diff --git a/TagsCloudContainer.Core/Infrastructure/Preprocessing/BoringWordsFilter.cs b/TagsCloudContainer.Core/Infrastructure/Preprocessing/BoringWordsFilter.cs index 4195a42a3..4f1d01e37 100644 --- a/TagsCloudContainer.Core/Infrastructure/Preprocessing/BoringWordsFilter.cs +++ b/TagsCloudContainer.Core/Infrastructure/Preprocessing/BoringWordsFilter.cs @@ -1,41 +1,22 @@ +using ResultOf; namespace TagsCloudContainer.Core.Infrastructure.Preprocessing; public class BoringWordsFilter : ITextPreprocessor { - private readonly IWordFilter[] _filters; + private readonly IBoringWordsProvider provider; - public BoringWordsFilter(IEnumerable? boringWords) + public BoringWordsFilter(IBoringWordsProvider provider) { - var boringWords1 = new HashSet( - boringWords ?? DefaultBoringWords, - StringComparer.OrdinalIgnoreCase); - - _filters = - [ - new BoringWordFilter(boringWords1) - ]; + this.provider = provider; } - private static readonly string[] DefaultBoringWords = - [ - "и", "в", "не", "на", "я", "он", "с", "что", "а", "по", - "это", "как", "но", "они", "к", "у", "же", "вы", "за", - "бы", "о", "из", "от", "то", "все", "его", "до", "для", - "мы", "при", "так", "та", "их", "чем", "или", "быть", - "вот", "от", "под", "ну", "ни", "да", "ли", "если", "еще" - ]; - - public IReadOnlyList Process(IEnumerable words) - { - return words - .Where(word => _filters.All(filter - => !filter.ShouldExclude(word))).ToArray(); - } - - private class BoringWordFilter(HashSet boringWords) : IWordFilter + public Result> Process(IEnumerable words) { - public bool ShouldExclude(string word) + return provider.GetWords().Then(boringWords => { - return boringWords.Contains(word); - } + var set = new HashSet(boringWords, + StringComparer.OrdinalIgnoreCase); + var filtered = words.Where(word => !set.Contains(word)).ToArray(); + return Result.Ok>(filtered); + }); } } \ No newline at end of file diff --git a/TagsCloudContainer.Core/Infrastructure/Preprocessing/DefaultBoringWordsProvider.cs b/TagsCloudContainer.Core/Infrastructure/Preprocessing/DefaultBoringWordsProvider.cs new file mode 100644 index 000000000..87fcc4a1e --- /dev/null +++ b/TagsCloudContainer.Core/Infrastructure/Preprocessing/DefaultBoringWordsProvider.cs @@ -0,0 +1,16 @@ +using ResultOf; +namespace TagsCloudContainer.Core.Infrastructure.Preprocessing; + +public class DefaultBoringWordsProvider : IBoringWordsProvider +{ + public Result GetWords() => Result.Ok(DefaultBoringWords); + + private static readonly string[] DefaultBoringWords = + [ + "и", "в", "не", "на", "я", "он", "с", "что", "а", "по", + "это", "как", "но", "они", "к", "у", "же", "вы", "за", + "бы", "о", "из", "от", "то", "все", "его", "до", "для", + "мы", "при", "так", "та", "их", "чем", "или", "быть", + "вот", "от", "под", "ну", "ни", "да", "ли", "если", "еще" + ]; +} \ No newline at end of file diff --git a/TagsCloudContainer.Core/Infrastructure/Preprocessing/EmptyBoringWordsProvider.cs b/TagsCloudContainer.Core/Infrastructure/Preprocessing/EmptyBoringWordsProvider.cs index fae981294..1948ee457 100644 --- a/TagsCloudContainer.Core/Infrastructure/Preprocessing/EmptyBoringWordsProvider.cs +++ b/TagsCloudContainer.Core/Infrastructure/Preprocessing/EmptyBoringWordsProvider.cs @@ -1,6 +1,7 @@ +using ResultOf; namespace TagsCloudContainer.Core.Infrastructure.Preprocessing; public class EmptyBoringWordsProvider : IBoringWordsProvider { - public string[] GetWords() => []; + public Result GetWords() => Result.Ok(Array.Empty()); } \ No newline at end of file diff --git a/TagsCloudContainer.Core/Infrastructure/Preprocessing/FileBoringWordsProvider.cs b/TagsCloudContainer.Core/Infrastructure/Preprocessing/FileBoringWordsProvider.cs index 6ea4f6174..ff0737c43 100644 --- a/TagsCloudContainer.Core/Infrastructure/Preprocessing/FileBoringWordsProvider.cs +++ b/TagsCloudContainer.Core/Infrastructure/Preprocessing/FileBoringWordsProvider.cs @@ -1,6 +1,18 @@ +using ResultOf; namespace TagsCloudContainer.Core.Infrastructure.Preprocessing; public class FileBoringWordsProvider(string path) : IBoringWordsProvider { - public string[] GetWords() => File.ReadAllLines(path); + public Result GetWords() + { + if (string.IsNullOrWhiteSpace(path)) + return Result.Ok(Array.Empty()); + + if (!File.Exists(path)) + return Result.Fail($"Boring words file not found {path}"); + + return Result.Of(() => File.ReadAllLines(path), + $"Failed to read boring words file {path}"); + } + } \ No newline at end of file diff --git a/TagsCloudContainer.Core/Infrastructure/Preprocessing/IBoringWordsProvider.cs b/TagsCloudContainer.Core/Infrastructure/Preprocessing/IBoringWordsProvider.cs index db284ca25..c63f43d73 100644 --- a/TagsCloudContainer.Core/Infrastructure/Preprocessing/IBoringWordsProvider.cs +++ b/TagsCloudContainer.Core/Infrastructure/Preprocessing/IBoringWordsProvider.cs @@ -1,6 +1,6 @@ +using ResultOf; namespace TagsCloudContainer.Core.Infrastructure.Preprocessing; - public interface IBoringWordsProvider { - string[] GetWords(); + Result GetWords(); } \ No newline at end of file diff --git a/TagsCloudContainer.Core/Infrastructure/Preprocessing/ITextPreprocessor.cs b/TagsCloudContainer.Core/Infrastructure/Preprocessing/ITextPreprocessor.cs index 0ba9b0e87..53905ebfe 100644 --- a/TagsCloudContainer.Core/Infrastructure/Preprocessing/ITextPreprocessor.cs +++ b/TagsCloudContainer.Core/Infrastructure/Preprocessing/ITextPreprocessor.cs @@ -1,5 +1,6 @@ +using ResultOf; namespace TagsCloudContainer.Core.Infrastructure.Preprocessing; public interface ITextPreprocessor { - IReadOnlyList Process(IEnumerable words); + Result> Process(IEnumerable words); } \ No newline at end of file diff --git a/TagsCloudContainer.Core/Infrastructure/Preprocessing/LowerCaseNormalizer.cs b/TagsCloudContainer.Core/Infrastructure/Preprocessing/LowerCaseNormalizer.cs index bed82bc6b..f6e9d62e5 100644 --- a/TagsCloudContainer.Core/Infrastructure/Preprocessing/LowerCaseNormalizer.cs +++ b/TagsCloudContainer.Core/Infrastructure/Preprocessing/LowerCaseNormalizer.cs @@ -1,8 +1,8 @@ +using ResultOf; namespace TagsCloudContainer.Core.Infrastructure.Preprocessing; public class LowerCaseNormalizer : ITextPreprocessor { - public IReadOnlyList Process(IEnumerable words) - { - return words.Select(word => word.ToLowerInvariant()).ToArray(); - } + public Result> Process(IEnumerable words) => + Result.Ok>(words.Select(w => + w.ToLowerInvariant()).ToArray()); } \ No newline at end of file diff --git a/TagsCloudContainer.Core/Infrastructure/Reading/DocxTextReader.cs b/TagsCloudContainer.Core/Infrastructure/Reading/DocxTextReader.cs index 94a8d80cf..a598da30f 100644 --- a/TagsCloudContainer.Core/Infrastructure/Reading/DocxTextReader.cs +++ b/TagsCloudContainer.Core/Infrastructure/Reading/DocxTextReader.cs @@ -1,30 +1,34 @@ using DocumentFormat.OpenXml.Packaging; using DocumentFormat.OpenXml.Wordprocessing; - +using ResultOf; namespace TagsCloudContainer.Core.Infrastructure.Reading; public class DocxTextReader : IFileTextReader { public bool CanRead(string filePath) => filePath.EndsWith(".docx", StringComparison.OrdinalIgnoreCase); - public IReadOnlyList ReadWords(string filePath) + public Result> ReadWords(string filePath) { if (!File.Exists(filePath)) - throw new FileNotFoundException($"File not found: {filePath}"); + return Result.Fail>($"File not found: {filePath}"); + - using var doc = WordprocessingDocument.Open(filePath, false); - var body = doc.MainDocumentPart?.Document?.Body; - if (body == null) - return []; + return Result.Of(() => + { + using var doc = WordprocessingDocument.Open(filePath, false); + var body = doc.MainDocumentPart?.Document?.Body; + if (body == null) + return []; - var words = new List(); - foreach (var line in body.Descendants().Select(t => t.Text)) - { - if (string.IsNullOrWhiteSpace(line)) - continue; - words.AddRange(line.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries)); - } + var words = new List(); + foreach (var line in body.Descendants().Select(t => t.Text)) + { + if (string.IsNullOrWhiteSpace(line)) + continue; + words.AddRange(line.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries)); + } - return words; + return (IReadOnlyList)words; + }, $"Failed to read docx file: {filePath}"); } } diff --git a/TagsCloudContainer.Core/Infrastructure/Reading/ITextReader.cs b/TagsCloudContainer.Core/Infrastructure/Reading/ITextReader.cs index aa6521ccc..1d4e2fc08 100644 --- a/TagsCloudContainer.Core/Infrastructure/Reading/ITextReader.cs +++ b/TagsCloudContainer.Core/Infrastructure/Reading/ITextReader.cs @@ -1,9 +1,10 @@ +using ResultOf; namespace TagsCloudContainer.Core.Infrastructure.Reading; public interface ITextReader { bool CanRead(string filePath); - IReadOnlyList ReadWords(string filePath); + Result> ReadWords(string filePath); } public interface IFileTextReader : ITextReader diff --git a/TagsCloudContainer.Core/Infrastructure/Reading/MultiFormatTextReader.cs b/TagsCloudContainer.Core/Infrastructure/Reading/MultiFormatTextReader.cs index c07c3785e..09e62854c 100644 --- a/TagsCloudContainer.Core/Infrastructure/Reading/MultiFormatTextReader.cs +++ b/TagsCloudContainer.Core/Infrastructure/Reading/MultiFormatTextReader.cs @@ -1,3 +1,4 @@ +using ResultOf; namespace TagsCloudContainer.Core.Infrastructure.Reading; public class MultiFormatTextReader(IEnumerable readers) : ITextReader { @@ -6,11 +7,11 @@ public class MultiFormatTextReader(IEnumerable readers) : IText public bool CanRead(string filePath) => _readers.Any(r => r.CanRead(filePath)); - public IReadOnlyList ReadWords(string filePath) + public Result> ReadWords(string filePath) { var reader = _readers.FirstOrDefault(r => r.CanRead(filePath)); return reader == null ? - throw new NotSupportedException($"No reader for file {filePath}") - : reader.ReadWords(filePath); + Result.Fail>($"Unsupported file format: {filePath}") + : reader.ReadWords(filePath).RefineError("Failed to read input file"); } } diff --git a/TagsCloudContainer.Core/Infrastructure/Reading/TextFileReader.cs b/TagsCloudContainer.Core/Infrastructure/Reading/TextFileReader.cs index ad98bfa4a..b134bdbc7 100644 --- a/TagsCloudContainer.Core/Infrastructure/Reading/TextFileReader.cs +++ b/TagsCloudContainer.Core/Infrastructure/Reading/TextFileReader.cs @@ -1,3 +1,4 @@ +using ResultOf; namespace TagsCloudContainer.Core.Infrastructure.Reading; public class TextFileReader : IFileTextReader { @@ -6,13 +7,15 @@ public bool CanRead(string filePath) return filePath.EndsWith(".txt", StringComparison.OrdinalIgnoreCase); } - public IReadOnlyList ReadWords(string filePath) + public Result> ReadWords(string filePath) { if (!File.Exists(filePath)) - throw new FileNotFoundException($"File not found: {filePath}"); + return Result.Fail>($"File not found: {filePath}"); - return File.ReadLines(filePath) - .Where(line => !string.IsNullOrWhiteSpace(line)) - .Select(line => line.Trim()).ToArray(); + + return Result.Of>(() => File.ReadLines(filePath) + .Where(line => !string.IsNullOrWhiteSpace(line)) + .Select(line => line.Trim()).ToArray(), + $"Failed to read file: {filePath}"); } } \ No newline at end of file diff --git a/TagsCloudContainer.Core/Infrastructure/Rendering/ITagCloudRenderer.cs b/TagsCloudContainer.Core/Infrastructure/Rendering/ITagCloudRenderer.cs index e1fb0fbe0..222b08835 100644 --- a/TagsCloudContainer.Core/Infrastructure/Rendering/ITagCloudRenderer.cs +++ b/TagsCloudContainer.Core/Infrastructure/Rendering/ITagCloudRenderer.cs @@ -1,9 +1,10 @@ +using ResultOf; using SkiaSharp; using TagsCloudContainer.Core.Models; namespace TagsCloudContainer.Core.Infrastructure.Rendering; public interface ITagCloudRenderer { - SKImage Render(IEnumerable layoutWords, LayoutOptions options); - void SaveToFile(SKImage image, string filePath); + Result Render(IEnumerable layoutWords, LayoutOptions options); + Result SaveToFile(SKImage image, string filePath); } \ No newline at end of file diff --git a/TagsCloudContainer.Core/Infrastructure/Rendering/PngRenderer.cs b/TagsCloudContainer.Core/Infrastructure/Rendering/PngRenderer.cs index 7a606dd1c..81732c4af 100644 --- a/TagsCloudContainer.Core/Infrastructure/Rendering/PngRenderer.cs +++ b/TagsCloudContainer.Core/Infrastructure/Rendering/PngRenderer.cs @@ -1,38 +1,53 @@ +using ResultOf; using SkiaSharp; using TagsCloudContainer.Core.Models; namespace TagsCloudContainer.Core.Infrastructure.Rendering; public class PngRenderer : ITagCloudRenderer { - public SKImage Render(IEnumerable layoutWords, LayoutOptions options) + public Result Render(IEnumerable layoutWords, LayoutOptions options) { - var info = new SKImageInfo(options.Width, options.Height); - using var surface = SKSurface.Create(info); - var canvas = surface.Canvas; - - canvas.Clear(options.BackgroundColor); - - foreach (var layoutWord in layoutWords) + return Result.Of(() => { - using var paint = new SKPaint(); - paint.Color = layoutWord.Color; - paint.Typeface = layoutWord.Typeface; - paint.TextSize = layoutWord.FontSize; - paint.IsAntialias = true; + var info = new SKImageInfo(options.Width, options.Height); + using var surface = SKSurface.Create(info); + if (surface == null) + throw new InvalidOperationException("Failed to create Skia surface"); - canvas.DrawText( - layoutWord.WordFrequency.Word, - layoutWord.Rectangle.Left, - layoutWord.Rectangle.Bottom, - paint); - } - return surface.Snapshot(); + var canvas = surface.Canvas; + canvas.Clear(options.BackgroundColor); + + foreach (var layoutWord in layoutWords) + { + using var paint = new SKPaint(); + paint.Color = layoutWord.Color; + paint.Typeface = layoutWord.Typeface; + paint.TextSize = layoutWord.FontSize; + paint.IsAntialias = true; + + canvas.DrawText( + layoutWord.WordFrequency.Word, + layoutWord.Rectangle.Left, + layoutWord.Rectangle.Bottom, + paint); + } + + return surface.Snapshot(); + }, "Failed to render image"); } - public void SaveToFile(SKImage image, string filePath) + public Result SaveToFile(SKImage image, string filePath) { - using var data = image.Encode(SKEncodedImageFormat.Png, 100); - using var stream = File.OpenWrite(filePath); - data.SaveTo(stream); + var dir = Path.GetDirectoryName(filePath); + if (!string.IsNullOrWhiteSpace(dir) && !Directory.Exists(dir)) + return Result.Fail($"Output directory doesn't exist {dir}"); + return Result.OfAction(() => + { + using var data = image.Encode(SKEncodedImageFormat.Png, 100); + if (data == null) + throw new InvalidOperationException("Failed to encode image"); + using var stream = File.OpenWrite(filePath); + data.SaveTo(stream); + }, $"Failed to save file {filePath}"); } } \ No newline at end of file diff --git a/TagsCloudContainer.Core/Models/LayoutOptions.cs b/TagsCloudContainer.Core/Models/LayoutOptions.cs index 0521184cb..ec04bf84b 100644 --- a/TagsCloudContainer.Core/Models/LayoutOptions.cs +++ b/TagsCloudContainer.Core/Models/LayoutOptions.cs @@ -7,10 +7,5 @@ public class LayoutOptions public int Height { get; init; } = 600; public string FontFamily { get; init; } = "Arial"; public SKColor BackgroundColor { get; } = SKColors.White; - public SKPoint Center { get; } - - public LayoutOptions() - { - Center = new SKPoint(Width / 2f, Height / 2f); - } + public SKPoint Center => new(Width / 2f, Height / 2f); } \ No newline at end of file diff --git a/TagsCloudContainer.Core/Result.cs b/TagsCloudContainer.Core/Result.cs new file mode 100644 index 000000000..a5f85f400 --- /dev/null +++ b/TagsCloudContainer.Core/Result.cs @@ -0,0 +1,123 @@ +namespace ResultOf; + public class None + { + private None() + { + } + } + + public struct Result + { + public Result(string error, T value = default(T)) + { + 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, + 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 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); + } + } \ No newline at end of file From cad8286ec5bd505f61d3d0460f65d20982ff8d96 Mon Sep 17 00:00:00 2001 From: Krotkaya Date: Fri, 16 Jan 2026 19:59:01 +0500 Subject: [PATCH 3/6] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB?= =?UTF-8?q?=D0=B0=20=D0=B2=D0=B0=D0=BB=D0=B8=D0=B4=D0=B0=D1=86=D0=B8=D1=8E?= =?UTF-8?q?=20=D0=B2=D1=85=D0=BE=D0=B4=D0=BD=D1=8B=D1=85=20=D0=BF=D0=B0?= =?UTF-8?q?=D1=80=D0=B0=D0=BC=D0=B5=D1=82=D1=80=D0=BE=D0=B2,=20=D0=B7?= =?UTF-8?q?=D0=B0=D0=BF=D1=83=D1=81=D0=BA=20=D1=87=D0=B5=D1=80=D0=B5=D0=B7?= =?UTF-8?q?=20Result=20=D0=B8=20=D0=B5=D0=B4=D0=B8=D0=BD=D1=8B=D0=B9=20?= =?UTF-8?q?=D0=B2=D1=8B=D0=B2=D0=BE=D0=B4=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BE?= =?UTF-8?q?=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CommandLineOptions.cs | 4 + TagsCloudContainer.Console/ConsoleClient.cs | 37 ++-- TagsCloudContainer.Console/Program.cs | 159 +++++++++++++----- 3 files changed, 141 insertions(+), 59 deletions(-) diff --git a/TagsCloudContainer.Console/CommandLineOptions.cs b/TagsCloudContainer.Console/CommandLineOptions.cs index 6704141d0..b696d404a 100644 --- a/TagsCloudContainer.Console/CommandLineOptions.cs +++ b/TagsCloudContainer.Console/CommandLineOptions.cs @@ -35,4 +35,8 @@ public class CommandLineOptions [Option("algorithm", Default = TagCloudAlgorithmType.Spiral, HelpText = "Algorithm: Spiral | Tight")] public TagCloudAlgorithmType Algorithm { get; init; } = TagCloudAlgorithmType.Spiral; + + [Option("settings", HelpText = "Path to JSON settings file")] + public string? SettingsFile { get; init; } + } diff --git a/TagsCloudContainer.Console/ConsoleClient.cs b/TagsCloudContainer.Console/ConsoleClient.cs index 55a04cb09..69d0a1158 100644 --- a/TagsCloudContainer.Console/ConsoleClient.cs +++ b/TagsCloudContainer.Console/ConsoleClient.cs @@ -1,3 +1,4 @@ +using ResultOf; using TagsCloudContainer.Core.Infrastructure.Analysis; using TagsCloudContainer.Core.Infrastructure.Layout; using TagsCloudContainer.Core.Infrastructure.Preprocessing; @@ -13,29 +14,29 @@ public class ConsoleClient( ITagCloudAlgorithm algorithm, ITagCloudRenderer renderer) { - public void GenerateTagCloud( + public Result GenerateTagCloud( string inputFile, string outputFile, LayoutOptions options) { System.Console.WriteLine($"Reading words from {inputFile}..."); - var words = textReader.ReadWords(inputFile).ToArray(); - System.Console.WriteLine("Processing words..."); - var processedWords = preprocessor.Process(words).ToArray(); - - System.Console.WriteLine("Analyzing word frequencies..."); - var frequencies = analyzer.Analyze(processedWords).ToArray(); - - System.Console.WriteLine("Arranging words in cloud..."); - var layoutWords = algorithm.Arrange(frequencies, options).ToArray(); - - System.Console.WriteLine($"Rendering image ({options.Width}x{options.Height})..."); - var image = renderer.Render(layoutWords, options); - - renderer.SaveToFile(image, outputFile); - - System.Console.WriteLine($"Tag cloud saved to {outputFile}"); - System.Console.WriteLine($"Total words processed: {frequencies.Length}"); + return textReader.ReadWords(inputFile) + .Then(words => preprocessor.Process(words)) + .Then(words => + { + if (words.Count == 0) + return Result.Fail>("No words to build a tag cloud"); + return Result.Ok(words); + }) + .Then(words => analyzer.Analyze(words)) + .Then(freqs => algorithm.Arrange(freqs, options)) + .Then(layout => + { + System.Console.WriteLine($"Rendering image ({options.Width} x{options.Height})..."); + return renderer.Render(layout, options); + }) + .Then(image => renderer.SaveToFile(image, outputFile)) + .RefineError("Tag cloud generation failed"); } } \ No newline at end of file diff --git a/TagsCloudContainer.Console/Program.cs b/TagsCloudContainer.Console/Program.cs index 4c63e78fc..85c62ed64 100644 --- a/TagsCloudContainer.Console/Program.cs +++ b/TagsCloudContainer.Console/Program.cs @@ -1,58 +1,135 @@ -using Autofac; +using System.Text.Json; +using Autofac; using CommandLine; using TagsCloudContainer.Console; using TagsCloudContainer.Core.DI; using TagsCloudContainer.Core.Models; +using ResultOf; + var parser = new Parser(with => with.HelpWriter = Console.Out); return parser.ParseArguments(args) .MapResult( options => - { - try - { - var settings = new TagCloudSettings - { - Width = options.Width, - Height = options.Height, - FontFamily = options.FontFamily, - ColorScheme = options.ColorScheme, - MinFontSize = options.MinFontSize, - MaxFontSize = options.MaxFontSize, - Algorithm = options.Algorithm, - BoringWordsPath = options.BoringWordsFile - }; - - var builder = new ContainerBuilder(); - builder.RegisterModule(new AutofacModule(settings)); - builder.RegisterInstance(options).As(); - builder.RegisterType().SingleInstance(); - - var container = builder.Build(); - - var layoutOptions = new LayoutOptions + { + return LoadSettings(options) + .Then(settings => { - Width = options.Width, - Height = options.Height, - FontFamily = options.FontFamily - }; - - var client = container.Resolve(); - client.GenerateTagCloud(options.InputFile, options.OutputFile, layoutOptions); - - Console.WriteLine("Tag cloud generated successfully!"); - return 0; - } - catch (Exception ex) - { - Console.Error.WriteLine($"Error: {ex.Message}"); - return 1; - } + var layoutOptions = new LayoutOptions + { + Width = settings.Width, + Height = settings.Height, + FontFamily = settings.FontFamily + }; + + return Result.Of(() => + { + var builder = new ContainerBuilder(); + builder.RegisterModule(new AutofacModule(settings)); + builder.RegisterInstance(options).As(); + builder.RegisterType().SingleInstance(); + + var container = builder.Build(); + return container.Resolve(); + }, "Failed to initialize DI container") + .Then(client => + client.GenerateTagCloud(options.InputFile, options.OutputFile, layoutOptions)); + }) + .OnFail(err => Console.Error.WriteLine("Error: " + err)) + .IsSuccess ? 0 : 1; }, errors => { foreach (var e in errors) Console.Error.WriteLine(e.ToString()); return 1; - }); \ No newline at end of file + }); + + +static Result ValidateOptions(CommandLineOptions o) +{ + if (o.Width <= 0 || o.Height <= 0) + return Result.Fail("Invalid image size. Width and height must be positive"); + + if (o.MinFontSize <= 0 || o.MaxFontSize <= 0) + return Result.Fail("Invalid font size. Min/Max must be positive"); + + if (o.MinFontSize > o.MaxFontSize) + return Result.Fail("Invalid font size range. MinFontSize > MaxFontSize"); + + if (string.IsNullOrWhiteSpace(o.InputFile)) + return Result.Fail("Input file is not specified"); + + if (string.IsNullOrWhiteSpace(o.OutputFile)) + return Result.Fail("Output file is not specified"); + + return Result.Ok(new TagCloudSettings + { + Width = o.Width, + Height = o.Height, + FontFamily = o.FontFamily, + ColorScheme = o.ColorScheme, + MinFontSize = o.MinFontSize, + MaxFontSize = o.MaxFontSize, + Algorithm = o.Algorithm, + BoringWordsPath = o.BoringWordsFile + }); +} + static Result LoadSettings(CommandLineOptions o) + { + if (string.IsNullOrWhiteSpace(o.SettingsFile)) + return ValidateOptions(o); // old CLI path + + if (!File.Exists(o.SettingsFile)) + return Result.Fail($"Settings file not found: {o.SettingsFile}"); + + return Result.Of(() => + { + var json = File.ReadAllText(o.SettingsFile); + var settings = JsonSerializer.Deserialize( + json, + new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + if (settings == null) + throw new InvalidOperationException("Settings file is empty or invalid."); + return settings; + }, $"Failed to read settings file: {o.SettingsFile}") + .Then(ValidateSettings) + .Then(settings => Result.Ok(OverrideBoringWords(settings, o.BoringWordsFile))); + } + + static Result ValidateSettings(TagCloudSettings s) + { + if (s.Width <= 0 || s.Height <= 0) + return Result.Fail("Invalid image size. Width and height must be positive"); + + if (s.MinFontSize <= 0 || s.MaxFontSize <= 0) + return Result.Fail("Invalid font size. Min/Max must be positive"); + + if (s.MinFontSize > s.MaxFontSize) + return Result.Fail("Invalid font size range. MinFontSize > MaxFontSize"); + + if (string.IsNullOrWhiteSpace(s.FontFamily)) + return Result.Fail("Font family is not specified"); + + return Result.Ok(s); + } + + static TagCloudSettings OverrideBoringWords(TagCloudSettings s, string? path) + { + if (string.IsNullOrWhiteSpace(path)) + return s; + + return new TagCloudSettings + { + Width = s.Width, + Height = s.Height, + FontFamily = s.FontFamily, + ColorScheme = s.ColorScheme, + MinFontSize = s.MinFontSize, + MaxFontSize = s.MaxFontSize, + Algorithm = s.Algorithm, + BoringWordsPath = path + }; + } + From 812771d0b048768fab71b7e954e89f8115556458 Mon Sep 17 00:00:00 2001 From: Krotkaya Date: Fri, 16 Jan 2026 20:04:30 +0500 Subject: [PATCH 4/6] =?UTF-8?q?=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=B8=D0=BB?= =?UTF-8?q?=D0=B0=20=D1=82=D0=B5=D1=81=D1=82=D1=8B=20=D0=BF=D0=BE=D0=B4=20?= =?UTF-8?q?=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- TagsCloudContainer.Tests/PngRendererTests.cs | 4 ++- .../SpiralTagCloudAlgorithmTests.cs | 32 +++++++++++++++++-- .../TextFileReaderTests.cs | 24 ++++++++------ 3 files changed, 47 insertions(+), 13 deletions(-) diff --git a/TagsCloudContainer.Tests/PngRendererTests.cs b/TagsCloudContainer.Tests/PngRendererTests.cs index 0b4d5e795..6e22b91aa 100644 --- a/TagsCloudContainer.Tests/PngRendererTests.cs +++ b/TagsCloudContainer.Tests/PngRendererTests.cs @@ -25,7 +25,9 @@ public void SaveToFile_ShouldCreateNonEmptyPng() SKColors.Blue, new SKRect(80, 10, 150, 40)) }; - using var image = renderer.Render(layoutWords, options); + var renderResult = renderer.Render(layoutWords, options); + renderResult.IsSuccess.Should().BeTrue(); + using var image = renderResult.GetValueOrThrow(); var tempPath = Path.GetTempFileName(); try diff --git a/TagsCloudContainer.Tests/SpiralTagCloudAlgorithmTests.cs b/TagsCloudContainer.Tests/SpiralTagCloudAlgorithmTests.cs index 5a0b2316d..7156ea495 100644 --- a/TagsCloudContainer.Tests/SpiralTagCloudAlgorithmTests.cs +++ b/TagsCloudContainer.Tests/SpiralTagCloudAlgorithmTests.cs @@ -32,7 +32,10 @@ public void Arrange_ShouldPlaceAllWordsWithoutIntersections() new WordFrequency("three", 1) }; - var layoutWords = algorithm.Arrange(frequencies, options).ToArray(); + var result = algorithm.Arrange(frequencies, options); + + result.IsSuccess.Should().BeTrue(); + var layoutWords = result.GetValueOrThrow().ToArray(); layoutWords.Should().HaveCount(frequencies.Length); @@ -44,6 +47,7 @@ public void Arrange_ShouldPlaceAllWordsWithoutIntersections() .Should().BeFalse($"rectangles {i} and {j} should not overlap"); } } + } [Test] @@ -60,9 +64,33 @@ public void Arrange_ShouldPlaceFirstWordAroundCenter() var frequencies = new[] { new WordFrequency("center", 10) }; - var layoutWords = algorithm.Arrange(frequencies, options).ToArray(); + var result = algorithm.Arrange(frequencies, options); + result.IsSuccess.Should().BeTrue(); + var layoutWords = result.GetValueOrThrow().ToArray(); layoutWords.Should().ContainSingle(); layoutWords[0].Rectangle.Contains(options.Center).Should().BeTrue(); } + + [Test] + public void Arrange_ShouldFail_WhenWordTooLargeForImage() + { + var fontCalculator = Substitute.For(); + fontCalculator + .Calculate(Arg.Any(), Arg.Any()).Returns(1000f); + + var colorScheme = Substitute.For(); + colorScheme.GetColor(Arg.Any()).Returns(SKColors.Black); + + var algorithm = new SpiralTagCloudAlgorithm(fontCalculator, colorScheme); + var options = new LayoutOptions(); + + var frequencies = new[] { new WordFrequency("qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq", 10) }; + + var result = algorithm.Arrange(frequencies, options); + + result.IsSuccess.Should().BeFalse(); + result.Error.Should().Contain("Tag cloud doesn't fit"); + } + } diff --git a/TagsCloudContainer.Tests/TextFileReaderTests.cs b/TagsCloudContainer.Tests/TextFileReaderTests.cs index 5c1665872..9b8e56bdd 100644 --- a/TagsCloudContainer.Tests/TextFileReaderTests.cs +++ b/TagsCloudContainer.Tests/TextFileReaderTests.cs @@ -32,28 +32,32 @@ public void ReadWords_ShouldReturnAllLines_FromExistingFile() { using var temp = new TempFile(new[] { "word1", "word2", "word3" }); - var words = _reader.ReadWords(temp.Path).ToArray(); + var words = _reader.ReadWords(temp.Path); - words.Should().Equal("word1", "word2", "word3"); + words.IsSuccess.Should().BeTrue(); + words.GetValueOrThrow().Should().Equal("word1", "word2", "word3"); } [Test] - public void - ReadWords_ShouldThrowFileNotFoundException_ForNonExistingFile() + public void ReadWords_ShouldReturnFail_ForNonExistingFile() { - _reader.Invoking(r => r.ReadWords("non_existing_file.txt").ToArray()) - .Should().Throw(); + var result = _reader.ReadWords("non_existing_file.txt"); + + result.IsSuccess.Should().BeFalse(); + result.Error.Should().Contain("File not found"); + } [Test] public void ReadWords_ShouldSkipEmptyLines() { - using var temp = new TempFile(new[] { "word1", "", "word2", " ", - "word3" }); + using var temp = new TempFile(new[] { "word1", "", "word2", " ", "word3" }); + + var words = _reader.ReadWords(temp.Path); - var words = _reader.ReadWords(temp.Path).ToArray(); + words.IsSuccess.Should().BeTrue(); + words.GetValueOrThrow().Should().Equal("word1", "word2", "word3"); - words.Should().Equal("word1", "word2", "word3"); } private sealed class TempFile : IDisposable From b4eb14288566408c3b9fff5867a2722b700e7312 Mon Sep 17 00:00:00 2001 From: Krotkaya Date: Mon, 19 Jan 2026 19:51:25 +0500 Subject: [PATCH 5/6] =?UTF-8?q?=D0=92=D0=BD=D0=B5=D1=81=D0=BB=D0=B0=20?= =?UTF-8?q?=D0=BF=D1=80=D0=B0=D0=B2=D0=BA=D0=B8=20=D1=81=D0=BE=D0=B3=D0=BB?= =?UTF-8?q?=D0=B0=D1=81=D0=BD=D0=BE=20=D0=B7=D0=B0=D0=BC=D0=B5=D1=87=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D1=8F=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CommandLineOptions.cs | 1 - TagsCloudContainer.Console/ConsoleClient.cs | 24 ++-- TagsCloudContainer.Console/Program.cs | 130 ++++++++---------- TagsCloudContainer.Core/DI/AutofacModule.cs | 16 +-- .../DI/CompositePreprocessor.cs | 2 - .../Coloring/ColorSchemeType.cs | 1 - .../Coloring/GradientColorScheme.cs | 2 - .../Layout/IFontSizeCalculator.cs | 1 - .../Layout/LinearFontSizeCalculator.cs | 2 - .../Layout/RectangleCloudLayouter.cs | 11 +- .../Layout/SpiralTagCloudAlgorithm.cs | 12 +- .../Layout/TagCloudAlgorithmType.cs | 1 - .../Layout/TightSpiralTagCloudAlgorithm.cs | 10 +- .../Preprocessing/BoringWordsFilter.cs | 10 +- .../DefaultBoringWordsProvider.cs | 2 +- .../Preprocessing/EmptyBoringWordsProvider.cs | 2 +- .../Preprocessing/FileBoringWordsProvider.cs | 5 +- .../Preprocessing/IBoringWordsProvider.cs | 1 + .../Preprocessing/ITextPreprocessor.cs | 1 + .../Preprocessing/IWordFilter.cs | 5 - .../Preprocessing/LowerCaseNormalizer.cs | 3 +- .../Infrastructure/Reading/DocxTextReader.cs | 12 +- .../Infrastructure/Reading/ITextReader.cs | 2 +- .../Reading/MultiFormatTextReader.cs | 5 +- .../Infrastructure/Reading/TextFileReader.cs | 4 +- .../Infrastructure/Rendering/PngRenderer.cs | 50 +++---- .../Models/TagCloudSettingsValidator.cs | 37 +++++ TagsCloudContainer.Core/Result.cs | 17 +-- TagsCloudContainer.Tests/PngRendererTests.cs | 2 +- .../SpiralTagCloudAlgorithmTests.cs | 1 - 30 files changed, 167 insertions(+), 205 deletions(-) delete mode 100644 TagsCloudContainer.Core/Infrastructure/Preprocessing/IWordFilter.cs create mode 100644 TagsCloudContainer.Core/Models/TagCloudSettingsValidator.cs diff --git a/TagsCloudContainer.Console/CommandLineOptions.cs b/TagsCloudContainer.Console/CommandLineOptions.cs index b696d404a..64889d37e 100644 --- a/TagsCloudContainer.Console/CommandLineOptions.cs +++ b/TagsCloudContainer.Console/CommandLineOptions.cs @@ -38,5 +38,4 @@ public class CommandLineOptions [Option("settings", HelpText = "Path to JSON settings file")] public string? SettingsFile { get; init; } - } diff --git a/TagsCloudContainer.Console/ConsoleClient.cs b/TagsCloudContainer.Console/ConsoleClient.cs index 69d0a1158..4a99998aa 100644 --- a/TagsCloudContainer.Console/ConsoleClient.cs +++ b/TagsCloudContainer.Console/ConsoleClient.cs @@ -22,21 +22,23 @@ public Result GenerateTagCloud( System.Console.WriteLine($"Reading words from {inputFile}..."); return textReader.ReadWords(inputFile) - .Then(words => preprocessor.Process(words)) - .Then(words => - { - if (words.Count == 0) - return Result.Fail>("No words to build a tag cloud"); - return Result.Ok(words); - }) - .Then(words => analyzer.Analyze(words)) - .Then(freqs => algorithm.Arrange(freqs, options)) + .Then(preprocessor.Process) + .Then(EnsureNotEmpty) + .Then(analyzer.Analyze) + .Then(freqs => + algorithm.Arrange(freqs, options)) .Then(layout => { - System.Console.WriteLine($"Rendering image ({options.Width} x{options.Height})..."); - return renderer.Render(layout, options); + System.Console.WriteLine($"Rendering image ({options.Width} x{options.Height})..."); + return renderer.Render(layout, options); }) .Then(image => renderer.SaveToFile(image, outputFile)) .RefineError("Tag cloud generation failed"); } + private static Result> EnsureNotEmpty(IReadOnlyList words) + { + return words.Count == 0 + ? Result.Fail>("No words to build a tag cloud") + : Result.Ok(words); + } } \ No newline at end of file diff --git a/TagsCloudContainer.Console/Program.cs b/TagsCloudContainer.Console/Program.cs index 85c62ed64..2e14846e3 100644 --- a/TagsCloudContainer.Console/Program.cs +++ b/TagsCloudContainer.Console/Program.cs @@ -6,14 +6,14 @@ using TagsCloudContainer.Core.Models; using ResultOf; - var parser = new Parser(with => with.HelpWriter = Console.Out); return parser.ParseArguments(args) .MapResult( options => { - return LoadSettings(options) + return ValidateCommandLineOptions(options) + .Then(LoadSettings) .Then(settings => { var layoutOptions = new LayoutOptions @@ -29,10 +29,9 @@ builder.RegisterModule(new AutofacModule(settings)); builder.RegisterInstance(options).As(); builder.RegisterType().SingleInstance(); - var container = builder.Build(); return container.Resolve(); - }, "Failed to initialize DI container") + }, "Failed to initialize DI container") .Then(client => client.GenerateTagCloud(options.InputFile, options.OutputFile, layoutOptions)); }) @@ -41,30 +40,23 @@ }, errors => { - foreach (var e in errors) - Console.Error.WriteLine(e.ToString()); + foreach (var e in errors) Console.Error.WriteLine(e.ToString()); return 1; }); - -static Result ValidateOptions(CommandLineOptions o) +static Result ValidateCommandLineOptions(CommandLineOptions o) { - if (o.Width <= 0 || o.Height <= 0) - return Result.Fail("Invalid image size. Width and height must be positive"); - - if (o.MinFontSize <= 0 || o.MaxFontSize <= 0) - return Result.Fail("Invalid font size. Min/Max must be positive"); - - if (o.MinFontSize > o.MaxFontSize) - return Result.Fail("Invalid font size range. MinFontSize > MaxFontSize"); - if (string.IsNullOrWhiteSpace(o.InputFile)) - return Result.Fail("Input file is not specified"); + return Result.Fail("Input file is not specified"); - if (string.IsNullOrWhiteSpace(o.OutputFile)) - return Result.Fail("Output file is not specified"); + return string.IsNullOrWhiteSpace(o.OutputFile) + ? Result.Fail("Output file is not specified") + : Result.Ok(o); +} - return Result.Ok(new TagCloudSettings +static TagCloudSettings CreateSettingsFromOptions(CommandLineOptions o) +{ + return new TagCloudSettings { Width = o.Width, Height = o.Height, @@ -74,62 +66,56 @@ static Result ValidateOptions(CommandLineOptions o) MaxFontSize = o.MaxFontSize, Algorithm = o.Algorithm, BoringWordsPath = o.BoringWordsFile - }); + }; } - static Result LoadSettings(CommandLineOptions o) - { - if (string.IsNullOrWhiteSpace(o.SettingsFile)) - return ValidateOptions(o); // old CLI path - - if (!File.Exists(o.SettingsFile)) - return Result.Fail($"Settings file not found: {o.SettingsFile}"); - - return Result.Of(() => - { - var json = File.ReadAllText(o.SettingsFile); - var settings = JsonSerializer.Deserialize( - json, - new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); - if (settings == null) - throw new InvalidOperationException("Settings file is empty or invalid."); - return settings; - }, $"Failed to read settings file: {o.SettingsFile}") - .Then(ValidateSettings) - .Then(settings => Result.Ok(OverrideBoringWords(settings, o.BoringWordsFile))); - } - static Result ValidateSettings(TagCloudSettings s) - { - if (s.Width <= 0 || s.Height <= 0) - return Result.Fail("Invalid image size. Width and height must be positive"); - - if (s.MinFontSize <= 0 || s.MaxFontSize <= 0) - return Result.Fail("Invalid font size. Min/Max must be positive"); +static Result LoadSettings(CommandLineOptions o) +{ + var validator = new TagCloudSettingsValidator(); - if (s.MinFontSize > s.MaxFontSize) - return Result.Fail("Invalid font size range. MinFontSize > MaxFontSize"); + if (string.IsNullOrWhiteSpace(o.SettingsFile)) + return validator.Validate(CreateSettingsFromOptions(o)); - if (string.IsNullOrWhiteSpace(s.FontFamily)) - return Result.Fail("Font family is not specified"); + if (!File.Exists(o.SettingsFile)) + return Result.Fail($"Settings file not found: {o.SettingsFile}"); - return Result.Ok(s); - } + return Result.Of(() => File.ReadAllText(o.SettingsFile), + $"Failed to read settings file: {o.SettingsFile}") + .Then(DeserializeSettings) + .Then(settings => Result.Ok(OverrideBoringWords(settings, o.BoringWordsFile))) + .Then(validator.Validate); - static TagCloudSettings OverrideBoringWords(TagCloudSettings s, string? path) - { - if (string.IsNullOrWhiteSpace(path)) - return s; + static Result DeserializeSettings(string json) + { + try + { + var settings = JsonSerializer.Deserialize( + json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + return settings == null + ? Result.Fail("Settings file is empty or invalid.") + : Result.Ok(settings); + } + catch (Exception e) + { + return Result.Fail($"Failed to parse settings file: {e.Message}"); + } + } +} - return new TagCloudSettings - { - Width = s.Width, - Height = s.Height, - FontFamily = s.FontFamily, - ColorScheme = s.ColorScheme, - MinFontSize = s.MinFontSize, - MaxFontSize = s.MaxFontSize, - Algorithm = s.Algorithm, - BoringWordsPath = path - }; - } +static TagCloudSettings OverrideBoringWords(TagCloudSettings s, string? path) +{ + if (string.IsNullOrWhiteSpace(path)) + return s; + return new TagCloudSettings + { + Width = s.Width, + Height = s.Height, + FontFamily = s.FontFamily, + ColorScheme = s.ColorScheme, + MinFontSize = s.MinFontSize, + MaxFontSize = s.MaxFontSize, + Algorithm = s.Algorithm, + BoringWordsPath = path + }; +} diff --git a/TagsCloudContainer.Core/DI/AutofacModule.cs b/TagsCloudContainer.Core/DI/AutofacModule.cs index f25c4e651..a364079ad 100644 --- a/TagsCloudContainer.Core/DI/AutofacModule.cs +++ b/TagsCloudContainer.Core/DI/AutofacModule.cs @@ -14,19 +14,14 @@ public class AutofacModule(TagCloudSettings settings) : Module protected override void Load(ContainerBuilder builder) { builder.RegisterInstance(settings).As(); - builder.RegisterType().As(); builder.RegisterType().As(); - - builder.RegisterType() - .As().SingleInstance(); - + builder.RegisterType() .As().SingleInstance(); builder.Register(_ => string.IsNullOrWhiteSpace(settings.BoringWordsPath) ? new DefaultBoringWordsProvider() : new FileBoringWordsProvider(settings.BoringWordsPath)) .SingleInstance(); - builder.Register(c => { var boringWords = c.Resolve(); @@ -35,36 +30,27 @@ protected override void Load(ContainerBuilder builder) new BoringWordsFilter(boringWords) ]); }).SingleInstance(); - builder.RegisterType().As().SingleInstance(); - builder.Register(_ => new LinearFontSizeCalculator(settings.MinFontSize, settings.MaxFontSize)).SingleInstance(); - builder.RegisterType() .Keyed(ColorSchemeType.Random) .SingleInstance(); - builder.Register(_ => new GradientColorScheme(SKColors.Blue, SKColors.LightBlue)) .Keyed(ColorSchemeType.Gradient) .SingleInstance(); - builder.Register(ctx => ctx.ResolveKeyed(settings.ColorScheme)) .SingleInstance(); - builder.RegisterType() .Keyed(TagCloudAlgorithmType.Spiral) .SingleInstance(); - builder.RegisterType() .Keyed(TagCloudAlgorithmType.Tight) .SingleInstance(); - builder.Register(ctx => ctx.ResolveKeyed(settings.Algorithm)) .SingleInstance(); - builder.RegisterType().As().SingleInstance(); } } diff --git a/TagsCloudContainer.Core/DI/CompositePreprocessor.cs b/TagsCloudContainer.Core/DI/CompositePreprocessor.cs index b546840cf..50f3282aa 100644 --- a/TagsCloudContainer.Core/DI/CompositePreprocessor.cs +++ b/TagsCloudContainer.Core/DI/CompositePreprocessor.cs @@ -10,6 +10,4 @@ public Result> Process(IEnumerable words) return preprocessors.Aggregate(current, (current1, preprocessor) => current1.Then(preprocessor.Process)); } - - } \ No newline at end of file diff --git a/TagsCloudContainer.Core/Infrastructure/Coloring/ColorSchemeType.cs b/TagsCloudContainer.Core/Infrastructure/Coloring/ColorSchemeType.cs index d8788a1d9..2864c6571 100644 --- a/TagsCloudContainer.Core/Infrastructure/Coloring/ColorSchemeType.cs +++ b/TagsCloudContainer.Core/Infrastructure/Coloring/ColorSchemeType.cs @@ -1,5 +1,4 @@ namespace TagsCloudContainer.Core.Infrastructure.Coloring; - public enum ColorSchemeType { Random, diff --git a/TagsCloudContainer.Core/Infrastructure/Coloring/GradientColorScheme.cs b/TagsCloudContainer.Core/Infrastructure/Coloring/GradientColorScheme.cs index fc3aee720..a7610398f 100644 --- a/TagsCloudContainer.Core/Infrastructure/Coloring/GradientColorScheme.cs +++ b/TagsCloudContainer.Core/Infrastructure/Coloring/GradientColorScheme.cs @@ -7,7 +7,6 @@ public SKColor GetColor(int seed) { var hash = Math.Abs(seed); var ratio = (hash % 100) / 100.0f; - return InterpolateColor(startColor, endColor, ratio); } @@ -16,7 +15,6 @@ private static SKColor InterpolateColor(SKColor start, SKColor end, float ratio) var r = (byte)(start.Red + (end.Red - start.Red) * ratio); var g = (byte)(start.Green + (end.Green - start.Green) * ratio); var b = (byte)(start.Blue + (end.Blue - start.Blue) * ratio); - return new SKColor(r, g, b); } } \ No newline at end of file diff --git a/TagsCloudContainer.Core/Infrastructure/Layout/IFontSizeCalculator.cs b/TagsCloudContainer.Core/Infrastructure/Layout/IFontSizeCalculator.cs index 340c90395..579226793 100644 --- a/TagsCloudContainer.Core/Infrastructure/Layout/IFontSizeCalculator.cs +++ b/TagsCloudContainer.Core/Infrastructure/Layout/IFontSizeCalculator.cs @@ -1,5 +1,4 @@ namespace TagsCloudContainer.Core.Infrastructure.Layout; - public interface IFontSizeCalculator { float Calculate(int frequency, int maxFrequency); diff --git a/TagsCloudContainer.Core/Infrastructure/Layout/LinearFontSizeCalculator.cs b/TagsCloudContainer.Core/Infrastructure/Layout/LinearFontSizeCalculator.cs index fa671263b..e23f75eaf 100644 --- a/TagsCloudContainer.Core/Infrastructure/Layout/LinearFontSizeCalculator.cs +++ b/TagsCloudContainer.Core/Infrastructure/Layout/LinearFontSizeCalculator.cs @@ -6,10 +6,8 @@ public float Calculate(int frequency, int maxFrequency) { if (maxFrequency <= 0) return minFontSize; - var ratio = (float)frequency / maxFrequency; var fontSize = minFontSize + (maxFontSize - minFontSize) * ratio; - return Math.Max(minFontSize, Math.Min(maxFontSize, fontSize)); } } diff --git a/TagsCloudContainer.Core/Infrastructure/Layout/RectangleCloudLayouter.cs b/TagsCloudContainer.Core/Infrastructure/Layout/RectangleCloudLayouter.cs index f509c4588..599e6e498 100644 --- a/TagsCloudContainer.Core/Infrastructure/Layout/RectangleCloudLayouter.cs +++ b/TagsCloudContainer.Core/Infrastructure/Layout/RectangleCloudLayouter.cs @@ -26,7 +26,6 @@ public Result PutNextRectangle(SKSize size, SKRect bounds, int maxAttemp return Result.Fail("Tag cloud does not fit the image"); rectangles.Add(first); return Result.Ok(first); - } private Result FindPlace(SKSize size, SKRect bounds, int @@ -43,7 +42,6 @@ private Result FindPlace(SKSize size, SKRect bounds, int return Result.Fail("Tag cloud does not fit the image"); } - private SKRect ShiftToCenter(SKRect rect) { while (true) @@ -51,11 +49,9 @@ private SKRect ShiftToCenter(SKRect rect) var direction = GetDirectionToCenter(rect); if (direction == SKPoint.Empty) return rect; - - var shifted = rect.OffsetClone(direction.X, direction.Y); + var shifted = OffsetClone(rect, direction.X, direction.Y); if (rectangles.Any(r => r.IntersectsWith(shifted))) return rect; - rect = shifted; } } @@ -82,10 +78,7 @@ private static SKRect CreateRect(SKPoint center, SKSize size) var top = center.Y - size.Height / 2; return new SKRect(left, top, left + size.Width, top + size.Height); } -} -internal static class SkRectExtensions -{ - public static SKRect OffsetClone(this SKRect rect, float dx, float dy) => + private static SKRect OffsetClone(SKRect rect, float dx, float dy) => new(rect.Left + dx, rect.Top + dy, rect.Right + dx, rect.Bottom + dy); } diff --git a/TagsCloudContainer.Core/Infrastructure/Layout/SpiralTagCloudAlgorithm.cs b/TagsCloudContainer.Core/Infrastructure/Layout/SpiralTagCloudAlgorithm.cs index ea2ceec8f..6778c0c2f 100644 --- a/TagsCloudContainer.Core/Infrastructure/Layout/SpiralTagCloudAlgorithm.cs +++ b/TagsCloudContainer.Core/Infrastructure/Layout/SpiralTagCloudAlgorithm.cs @@ -16,7 +16,7 @@ public Result> Arrange( var frequencies = wordFrequencies.ToArray(); if (frequencies.Length == 0) return - Result.Ok>(Array.Empty()); + Result.Ok>([]); var typeface = SKFontManager.Default.MatchFamily(options.FontFamily); if (typeface == null) @@ -31,10 +31,8 @@ public Result> Arrange( { var fontSize = fontSizeCalculator.Calculate( wordFrequency.Frequency, maxFrequency); - var size = MeasureWordSize(wordFrequency.Word, typeface, fontSize); var location = FindLocationForRectangle(size, options, bounds, placedRectangles); - if (location == SKPoint.Empty) return Result.Fail>( "Tag cloud doesn't fit the image"); @@ -42,11 +40,9 @@ public Result> Arrange( var rectangle = new SKRect(location.X, location.Y, location.X + size.Width, location.Y + size.Height); placedRectangles.Add(rectangle); - var color = colorScheme.GetColor(wordFrequency.Word.GetHashCode()); layoutWords.Add(new LayoutWord(wordFrequency, typeface, fontSize, color, rectangle)); } - return Result.Ok>(layoutWords); } @@ -58,7 +54,6 @@ private static SKSize MeasureWordSize( using var paint = new SKPaint(); paint.Typeface = typeface; paint.TextSize = fontSize; - var bounds = new SKRect(); paint.MeasureText(word, ref bounds); return new SKSize(bounds.Width, bounds.Height); @@ -74,19 +69,15 @@ private static SKPoint FindLocationForRectangle( const double radiusStep = 1; double angle = 0; double radius = 0; - for (var i = 0; i < 1000; i++) { var x = options.Center.X + (float)(radius * Math.Cos(angle)) - size.Width / 2; var y = options.Center.Y + (float)(radius * Math.Sin(angle)) - size.Height / 2; - var candidate = new SKRect(x, y, x + size.Width, y + size.Height); - var fits = candidate.Left >= bounds.Left && candidate.Top >= bounds.Top && candidate.Right <= bounds.Right && candidate.Bottom <= bounds.Bottom; - if (fits && !placedRectangles.Any(rect => rect.IntersectsWith(candidate))) return new SKPoint(x, y); @@ -94,7 +85,6 @@ private static SKPoint FindLocationForRectangle( angle += angleStep; radius += radiusStep; } - return SKPoint.Empty; } } diff --git a/TagsCloudContainer.Core/Infrastructure/Layout/TagCloudAlgorithmType.cs b/TagsCloudContainer.Core/Infrastructure/Layout/TagCloudAlgorithmType.cs index 99b1a30af..e59ec94e6 100644 --- a/TagsCloudContainer.Core/Infrastructure/Layout/TagCloudAlgorithmType.cs +++ b/TagsCloudContainer.Core/Infrastructure/Layout/TagCloudAlgorithmType.cs @@ -1,5 +1,4 @@ namespace TagsCloudContainer.Core.Infrastructure.Layout; - public enum TagCloudAlgorithmType { Spiral, diff --git a/TagsCloudContainer.Core/Infrastructure/Layout/TightSpiralTagCloudAlgorithm.cs b/TagsCloudContainer.Core/Infrastructure/Layout/TightSpiralTagCloudAlgorithm.cs index 4018b1164..d7a5742d3 100644 --- a/TagsCloudContainer.Core/Infrastructure/Layout/TightSpiralTagCloudAlgorithm.cs +++ b/TagsCloudContainer.Core/Infrastructure/Layout/TightSpiralTagCloudAlgorithm.cs @@ -33,17 +33,13 @@ public Result> Arrange(IEnumerable maxFrequency); var size = MeasureWordSize(wordFrequency.Word, typeface, fontSize); var rectResult = layouter.PutNextRectangle(size, bounds); - if (!rectResult.IsSuccess) + if (rectResult is { IsSuccess: false, Error: not null }) return Result.Fail>(rectResult.Error); var rect = rectResult.GetValueOrThrow(); - var color = - colorScheme.GetColor(wordFrequency.Word.GetHashCode()); - - layoutWords.Add(new LayoutWord(wordFrequency, typeface, fontSize, - color, rect)); + var color = colorScheme.GetColor(wordFrequency.Word.GetHashCode()); + layoutWords.Add(new LayoutWord(wordFrequency, typeface, fontSize, color, rect)); } - return Result.Ok>(layoutWords); } diff --git a/TagsCloudContainer.Core/Infrastructure/Preprocessing/BoringWordsFilter.cs b/TagsCloudContainer.Core/Infrastructure/Preprocessing/BoringWordsFilter.cs index 4f1d01e37..6b1e24563 100644 --- a/TagsCloudContainer.Core/Infrastructure/Preprocessing/BoringWordsFilter.cs +++ b/TagsCloudContainer.Core/Infrastructure/Preprocessing/BoringWordsFilter.cs @@ -1,14 +1,8 @@ using ResultOf; + namespace TagsCloudContainer.Core.Infrastructure.Preprocessing; -public class BoringWordsFilter : ITextPreprocessor +public class BoringWordsFilter(IBoringWordsProvider provider) : ITextPreprocessor { - private readonly IBoringWordsProvider provider; - - public BoringWordsFilter(IBoringWordsProvider provider) - { - this.provider = provider; - } - public Result> Process(IEnumerable words) { return provider.GetWords().Then(boringWords => diff --git a/TagsCloudContainer.Core/Infrastructure/Preprocessing/DefaultBoringWordsProvider.cs b/TagsCloudContainer.Core/Infrastructure/Preprocessing/DefaultBoringWordsProvider.cs index 87fcc4a1e..fab585046 100644 --- a/TagsCloudContainer.Core/Infrastructure/Preprocessing/DefaultBoringWordsProvider.cs +++ b/TagsCloudContainer.Core/Infrastructure/Preprocessing/DefaultBoringWordsProvider.cs @@ -1,6 +1,6 @@ using ResultOf; -namespace TagsCloudContainer.Core.Infrastructure.Preprocessing; +namespace TagsCloudContainer.Core.Infrastructure.Preprocessing; public class DefaultBoringWordsProvider : IBoringWordsProvider { public Result GetWords() => Result.Ok(DefaultBoringWords); diff --git a/TagsCloudContainer.Core/Infrastructure/Preprocessing/EmptyBoringWordsProvider.cs b/TagsCloudContainer.Core/Infrastructure/Preprocessing/EmptyBoringWordsProvider.cs index 1948ee457..757b0b324 100644 --- a/TagsCloudContainer.Core/Infrastructure/Preprocessing/EmptyBoringWordsProvider.cs +++ b/TagsCloudContainer.Core/Infrastructure/Preprocessing/EmptyBoringWordsProvider.cs @@ -1,6 +1,6 @@ using ResultOf; -namespace TagsCloudContainer.Core.Infrastructure.Preprocessing; +namespace TagsCloudContainer.Core.Infrastructure.Preprocessing; public class EmptyBoringWordsProvider : IBoringWordsProvider { public Result GetWords() => Result.Ok(Array.Empty()); diff --git a/TagsCloudContainer.Core/Infrastructure/Preprocessing/FileBoringWordsProvider.cs b/TagsCloudContainer.Core/Infrastructure/Preprocessing/FileBoringWordsProvider.cs index ff0737c43..77e9717a7 100644 --- a/TagsCloudContainer.Core/Infrastructure/Preprocessing/FileBoringWordsProvider.cs +++ b/TagsCloudContainer.Core/Infrastructure/Preprocessing/FileBoringWordsProvider.cs @@ -1,18 +1,15 @@ using ResultOf; -namespace TagsCloudContainer.Core.Infrastructure.Preprocessing; +namespace TagsCloudContainer.Core.Infrastructure.Preprocessing; public class FileBoringWordsProvider(string path) : IBoringWordsProvider { public Result GetWords() { if (string.IsNullOrWhiteSpace(path)) return Result.Ok(Array.Empty()); - if (!File.Exists(path)) return Result.Fail($"Boring words file not found {path}"); - return Result.Of(() => File.ReadAllLines(path), $"Failed to read boring words file {path}"); } - } \ No newline at end of file diff --git a/TagsCloudContainer.Core/Infrastructure/Preprocessing/IBoringWordsProvider.cs b/TagsCloudContainer.Core/Infrastructure/Preprocessing/IBoringWordsProvider.cs index c63f43d73..cc84652c0 100644 --- a/TagsCloudContainer.Core/Infrastructure/Preprocessing/IBoringWordsProvider.cs +++ b/TagsCloudContainer.Core/Infrastructure/Preprocessing/IBoringWordsProvider.cs @@ -1,4 +1,5 @@ using ResultOf; + namespace TagsCloudContainer.Core.Infrastructure.Preprocessing; public interface IBoringWordsProvider { diff --git a/TagsCloudContainer.Core/Infrastructure/Preprocessing/ITextPreprocessor.cs b/TagsCloudContainer.Core/Infrastructure/Preprocessing/ITextPreprocessor.cs index 53905ebfe..34b1a9b03 100644 --- a/TagsCloudContainer.Core/Infrastructure/Preprocessing/ITextPreprocessor.cs +++ b/TagsCloudContainer.Core/Infrastructure/Preprocessing/ITextPreprocessor.cs @@ -1,4 +1,5 @@ using ResultOf; + namespace TagsCloudContainer.Core.Infrastructure.Preprocessing; public interface ITextPreprocessor { diff --git a/TagsCloudContainer.Core/Infrastructure/Preprocessing/IWordFilter.cs b/TagsCloudContainer.Core/Infrastructure/Preprocessing/IWordFilter.cs deleted file mode 100644 index ae9cb5e1b..000000000 --- a/TagsCloudContainer.Core/Infrastructure/Preprocessing/IWordFilter.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace TagsCloudContainer.Core.Infrastructure.Preprocessing; -public interface IWordFilter -{ - bool ShouldExclude(string word); -} \ No newline at end of file diff --git a/TagsCloudContainer.Core/Infrastructure/Preprocessing/LowerCaseNormalizer.cs b/TagsCloudContainer.Core/Infrastructure/Preprocessing/LowerCaseNormalizer.cs index f6e9d62e5..d37d7b0c6 100644 --- a/TagsCloudContainer.Core/Infrastructure/Preprocessing/LowerCaseNormalizer.cs +++ b/TagsCloudContainer.Core/Infrastructure/Preprocessing/LowerCaseNormalizer.cs @@ -1,7 +1,8 @@ using ResultOf; + namespace TagsCloudContainer.Core.Infrastructure.Preprocessing; public class LowerCaseNormalizer : ITextPreprocessor -{ +{ public Result> Process(IEnumerable words) => Result.Ok>(words.Select(w => w.ToLowerInvariant()).ToArray()); diff --git a/TagsCloudContainer.Core/Infrastructure/Reading/DocxTextReader.cs b/TagsCloudContainer.Core/Infrastructure/Reading/DocxTextReader.cs index a598da30f..ce7b0bd49 100644 --- a/TagsCloudContainer.Core/Infrastructure/Reading/DocxTextReader.cs +++ b/TagsCloudContainer.Core/Infrastructure/Reading/DocxTextReader.cs @@ -1,25 +1,23 @@ using DocumentFormat.OpenXml.Packaging; using DocumentFormat.OpenXml.Wordprocessing; using ResultOf; + namespace TagsCloudContainer.Core.Infrastructure.Reading; public class DocxTextReader : IFileTextReader { public bool CanRead(string filePath) => filePath.EndsWith(".docx", StringComparison.OrdinalIgnoreCase); - public Result> ReadWords(string filePath) { if (!File.Exists(filePath)) return Result.Fail>($"File not found: {filePath}"); - - + return Result.Of(() => { using var doc = WordprocessingDocument.Open(filePath, false); - var body = doc.MainDocumentPart?.Document?.Body; + var body = doc.MainDocumentPart?.Document.Body; if (body == null) return []; - var words = new List(); foreach (var line in body.Descendants().Select(t => t.Text)) { @@ -27,8 +25,8 @@ public Result> ReadWords(string filePath) continue; words.AddRange(line.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries)); } - return (IReadOnlyList)words; - }, $"Failed to read docx file: {filePath}"); + }) + .RefineError($"Failed to read docx file: {filePath}"); } } diff --git a/TagsCloudContainer.Core/Infrastructure/Reading/ITextReader.cs b/TagsCloudContainer.Core/Infrastructure/Reading/ITextReader.cs index 1d4e2fc08..d58977be2 100644 --- a/TagsCloudContainer.Core/Infrastructure/Reading/ITextReader.cs +++ b/TagsCloudContainer.Core/Infrastructure/Reading/ITextReader.cs @@ -1,6 +1,6 @@ using ResultOf; -namespace TagsCloudContainer.Core.Infrastructure.Reading; +namespace TagsCloudContainer.Core.Infrastructure.Reading; public interface ITextReader { bool CanRead(string filePath); diff --git a/TagsCloudContainer.Core/Infrastructure/Reading/MultiFormatTextReader.cs b/TagsCloudContainer.Core/Infrastructure/Reading/MultiFormatTextReader.cs index 09e62854c..47fe63b1c 100644 --- a/TagsCloudContainer.Core/Infrastructure/Reading/MultiFormatTextReader.cs +++ b/TagsCloudContainer.Core/Infrastructure/Reading/MultiFormatTextReader.cs @@ -1,4 +1,5 @@ using ResultOf; + namespace TagsCloudContainer.Core.Infrastructure.Reading; public class MultiFormatTextReader(IEnumerable readers) : ITextReader { @@ -10,8 +11,8 @@ public bool CanRead(string filePath) => _readers.Any(r => public Result> ReadWords(string filePath) { var reader = _readers.FirstOrDefault(r => r.CanRead(filePath)); - return reader == null ? - Result.Fail>($"Unsupported file format: {filePath}") + return reader == null + ? Result.Fail>($"Unsupported file format: {filePath}") : reader.ReadWords(filePath).RefineError("Failed to read input file"); } } diff --git a/TagsCloudContainer.Core/Infrastructure/Reading/TextFileReader.cs b/TagsCloudContainer.Core/Infrastructure/Reading/TextFileReader.cs index b134bdbc7..cba473148 100644 --- a/TagsCloudContainer.Core/Infrastructure/Reading/TextFileReader.cs +++ b/TagsCloudContainer.Core/Infrastructure/Reading/TextFileReader.cs @@ -1,4 +1,5 @@ using ResultOf; + namespace TagsCloudContainer.Core.Infrastructure.Reading; public class TextFileReader : IFileTextReader { @@ -11,8 +12,7 @@ public Result> ReadWords(string filePath) { if (!File.Exists(filePath)) return Result.Fail>($"File not found: {filePath}"); - - + return Result.Of>(() => File.ReadLines(filePath) .Where(line => !string.IsNullOrWhiteSpace(line)) .Select(line => line.Trim()).ToArray(), diff --git a/TagsCloudContainer.Core/Infrastructure/Rendering/PngRenderer.cs b/TagsCloudContainer.Core/Infrastructure/Rendering/PngRenderer.cs index 81732c4af..68a102211 100644 --- a/TagsCloudContainer.Core/Infrastructure/Rendering/PngRenderer.cs +++ b/TagsCloudContainer.Core/Infrastructure/Rendering/PngRenderer.cs @@ -8,32 +8,31 @@ public class PngRenderer : ITagCloudRenderer public Result Render(IEnumerable layoutWords, LayoutOptions options) { return Result.Of(() => - { - var info = new SKImageInfo(options.Width, options.Height); - using var surface = SKSurface.Create(info); - if (surface == null) - throw new InvalidOperationException("Failed to create Skia surface"); - - var canvas = surface.Canvas; - canvas.Clear(options.BackgroundColor); - - foreach (var layoutWord in layoutWords) { - using var paint = new SKPaint(); - paint.Color = layoutWord.Color; - paint.Typeface = layoutWord.Typeface; - paint.TextSize = layoutWord.FontSize; - paint.IsAntialias = true; + var info = new SKImageInfo(options.Width, options.Height); + using var surface = SKSurface.Create(info); + if (surface == null) + throw new InvalidOperationException("Failed to create Skia surface"); + var canvas = surface.Canvas; + canvas.Clear(options.BackgroundColor); - canvas.DrawText( - layoutWord.WordFrequency.Word, - layoutWord.Rectangle.Left, - layoutWord.Rectangle.Bottom, - paint); - } + foreach (var layoutWord in layoutWords) + { + using var paint = new SKPaint(); + paint.Color = layoutWord.Color; + paint.Typeface = layoutWord.Typeface; + paint.TextSize = layoutWord.FontSize; + paint.IsAntialias = true; - return surface.Snapshot(); - }, "Failed to render image"); + canvas.DrawText( + layoutWord.WordFrequency.Word, + layoutWord.Rectangle.Left, + layoutWord.Rectangle.Bottom, + paint); + } + return surface.Snapshot(); + }) + .RefineError("Failed to render image"); } public Result SaveToFile(SKImage image, string filePath) @@ -48,6 +47,7 @@ public Result SaveToFile(SKImage image, string filePath) throw new InvalidOperationException("Failed to encode image"); using var stream = File.OpenWrite(filePath); data.SaveTo(stream); - }, $"Failed to save file {filePath}"); + }) + .RefineError($"Failed to save file {filePath}"); } -} \ No newline at end of file +} diff --git a/TagsCloudContainer.Core/Models/TagCloudSettingsValidator.cs b/TagsCloudContainer.Core/Models/TagCloudSettingsValidator.cs new file mode 100644 index 000000000..fbb480ae9 --- /dev/null +++ b/TagsCloudContainer.Core/Models/TagCloudSettingsValidator.cs @@ -0,0 +1,37 @@ +using ResultOf; + +namespace TagsCloudContainer.Core.Models; +public class TagCloudSettingsValidator +{ + private const int MaxImageSide = 10000; + private const int MaxFontSize = 500; + + public Result Validate(TagCloudSettings settings) + { + if (settings.Width <= 0 || settings.Height <= 0) + return Result.Fail("Invalid image size. Width and height must be positive"); + + if (settings.Width > MaxImageSide || settings.Height > MaxImageSide) + return Result.Fail( + $"Invalid image size. Max width/height is {MaxImageSide}"); + + if (settings.MinFontSize <= 0 || settings.MaxFontSize <= 0) + return Result.Fail("Invalid font size. Min/Max must be positive"); + + if (settings.MinFontSize > settings.MaxFontSize) + return Result.Fail("Invalid font size range. MinFontSize > MaxFontSize"); + + if (settings.MaxFontSize > MaxFontSize) + return Result.Fail($"Invalid font size. MaxFontSize exceeds {MaxFontSize}"); + + if (string.IsNullOrWhiteSpace(settings.FontFamily)) + return Result.Fail("Font family is not specified"); + + if (!string.IsNullOrWhiteSpace(settings.BoringWordsPath) && + !File.Exists(settings.BoringWordsPath)) + return Result.Fail( + $"Boring words file not found: {settings.BoringWordsPath}"); + + return Result.Ok(settings); + } +} diff --git a/TagsCloudContainer.Core/Result.cs b/TagsCloudContainer.Core/Result.cs index a5f85f400..640be3879 100644 --- a/TagsCloudContainer.Core/Result.cs +++ b/TagsCloudContainer.Core/Result.cs @@ -8,7 +8,7 @@ private None() public struct Result { - public Result(string error, T value = default(T)) + public Result(string? error, T? value = default) { Error = error; Value = value; @@ -18,8 +18,8 @@ public static implicit operator Result(T v) return Result.Ok(v); } - public string Error { get; } - internal T Value { get; } + public string? Error { get; } + internal T? Value { get; } public T GetValueOrThrow() { if (IsSuccess) return Value; @@ -30,12 +30,7 @@ public T GetValueOrThrow() public static class Result { - public static Result AsResult(this T value) - { - return Ok(value); - } - - public static Result Ok(T value) + public static Result Ok(T? value) { return new Result(null, value); } @@ -50,7 +45,7 @@ public static Result Fail(string e) return new Result(e); } - public static Result Of(Func f, string error = null) + public static Result Of(Func f, string? error = null) { try { @@ -62,7 +57,7 @@ public static Result Of(Func f, string error = null) } } - public static Result OfAction(Action f, string error = null) + public static Result OfAction(Action f, string? error = null) { try { diff --git a/TagsCloudContainer.Tests/PngRendererTests.cs b/TagsCloudContainer.Tests/PngRendererTests.cs index 6e22b91aa..10e967cda 100644 --- a/TagsCloudContainer.Tests/PngRendererTests.cs +++ b/TagsCloudContainer.Tests/PngRendererTests.cs @@ -29,7 +29,7 @@ public void SaveToFile_ShouldCreateNonEmptyPng() renderResult.IsSuccess.Should().BeTrue(); using var image = renderResult.GetValueOrThrow(); var tempPath = Path.GetTempFileName(); - + try { renderer.SaveToFile(image, tempPath); diff --git a/TagsCloudContainer.Tests/SpiralTagCloudAlgorithmTests.cs b/TagsCloudContainer.Tests/SpiralTagCloudAlgorithmTests.cs index 7156ea495..ecdf1eb03 100644 --- a/TagsCloudContainer.Tests/SpiralTagCloudAlgorithmTests.cs +++ b/TagsCloudContainer.Tests/SpiralTagCloudAlgorithmTests.cs @@ -8,7 +8,6 @@ using TagsCloudContainer.Core.Models; namespace TagsCloudContainer.Tests; - [TestFixture] public class SpiralTagCloudAlgorithmTests { From 98945c3de44b4b53c64f41687b9c65da009a9439 Mon Sep 17 00:00:00 2001 From: Krotkaya Date: Tue, 20 Jan 2026 12:51:39 +0500 Subject: [PATCH 6/6] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=B2=20=D0=B4=D0=BE=D0=BF=D0=BE?= =?UTF-8?q?=D0=BB=D0=BD=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BA=20=D0=BF=D1=80?= =?UTF-8?q?=D0=B5=D0=B4=D1=8B=D0=B4=D1=83=D1=89=D0=B8=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- TagsCloudContainer.Console/Program.cs | 23 ++++--- .../Models/TagCloudSettings.cs | 60 ++++++++++++++++--- .../Models/TagCloudSettingsDto.cs | 15 +++++ TagsCloudContainer.Core/Result.cs | 14 ----- 4 files changed, 76 insertions(+), 36 deletions(-) create mode 100644 TagsCloudContainer.Core/Models/TagCloudSettingsDto.cs diff --git a/TagsCloudContainer.Console/Program.cs b/TagsCloudContainer.Console/Program.cs index 2e14846e3..3cc63be6f 100644 --- a/TagsCloudContainer.Console/Program.cs +++ b/TagsCloudContainer.Console/Program.cs @@ -54,9 +54,9 @@ static Result ValidateCommandLineOptions(CommandLineOptions : Result.Ok(o); } -static TagCloudSettings CreateSettingsFromOptions(CommandLineOptions o) +static TagCloudSettingsDto CreateSettingsFromOptions(CommandLineOptions o) { - return new TagCloudSettings + return new TagCloudSettingsDto { Width = o.Width, Height = o.Height, @@ -71,10 +71,8 @@ static TagCloudSettings CreateSettingsFromOptions(CommandLineOptions o) static Result LoadSettings(CommandLineOptions o) { - var validator = new TagCloudSettingsValidator(); - if (string.IsNullOrWhiteSpace(o.SettingsFile)) - return validator.Validate(CreateSettingsFromOptions(o)); + return TagCloudSettings.Create(CreateSettingsFromOptions(o)); if (!File.Exists(o.SettingsFile)) return Result.Fail($"Settings file not found: {o.SettingsFile}"); @@ -82,32 +80,31 @@ static Result LoadSettings(CommandLineOptions o) return Result.Of(() => File.ReadAllText(o.SettingsFile), $"Failed to read settings file: {o.SettingsFile}") .Then(DeserializeSettings) - .Then(settings => Result.Ok(OverrideBoringWords(settings, o.BoringWordsFile))) - .Then(validator.Validate); + .Then(settings => TagCloudSettings.Create(OverrideBoringWords(settings, o.BoringWordsFile))); - static Result DeserializeSettings(string json) + static Result DeserializeSettings(string json) { try { - var settings = JsonSerializer.Deserialize( + var settings = JsonSerializer.Deserialize( json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); return settings == null - ? Result.Fail("Settings file is empty or invalid.") + ? Result.Fail("Settings file is empty or invalid.") : Result.Ok(settings); } catch (Exception e) { - return Result.Fail($"Failed to parse settings file: {e.Message}"); + return Result.Fail($"Failed to parse settings file: {e.Message}"); } } } -static TagCloudSettings OverrideBoringWords(TagCloudSettings s, string? path) +static TagCloudSettingsDto OverrideBoringWords(TagCloudSettingsDto s, string? path) { if (string.IsNullOrWhiteSpace(path)) return s; - return new TagCloudSettings + return new TagCloudSettingsDto { Width = s.Width, Height = s.Height, diff --git a/TagsCloudContainer.Core/Models/TagCloudSettings.cs b/TagsCloudContainer.Core/Models/TagCloudSettings.cs index a8a9e668d..7f7fd2b4e 100644 --- a/TagsCloudContainer.Core/Models/TagCloudSettings.cs +++ b/TagsCloudContainer.Core/Models/TagCloudSettings.cs @@ -1,15 +1,57 @@ +using ResultOf; using TagsCloudContainer.Core.Infrastructure.Coloring; using TagsCloudContainer.Core.Infrastructure.Layout; namespace TagsCloudContainer.Core.Models; -public class TagCloudSettings +public sealed class TagCloudSettings { - public int Width { get; set; } = 800; - public int Height { get; set; } = 600; - public string FontFamily { get; set; } = "Arial"; - public ColorSchemeType ColorScheme { get; init; } = ColorSchemeType.Random; - public TagCloudAlgorithmType Algorithm { get; init; } = TagCloudAlgorithmType.Spiral; - public int MinFontSize { get; init; } = 10; - public int MaxFontSize { get; init; } = 50; - public string? BoringWordsPath { get; init; } = string.Empty; + public int Width { get; } + public int Height { get; } + public string FontFamily { get; } + public ColorSchemeType ColorScheme { get; } + public TagCloudAlgorithmType Algorithm { get; } + public int MinFontSize { get; } + public int MaxFontSize { get; } + public string? BoringWordsPath { get; } + + private TagCloudSettings( + int width, + int height, + string fontFamily, + ColorSchemeType colorScheme, + TagCloudAlgorithmType algorithm, + int minFontSize, + int maxFontSize, + string? boringWordsPath) + { + Width = width; + Height = height; + FontFamily = fontFamily; + ColorScheme = colorScheme; + Algorithm = algorithm; + MinFontSize = minFontSize; + MaxFontSize = maxFontSize; + BoringWordsPath = boringWordsPath; + } + + public static Result Create(TagCloudSettingsDto? data) + { + if (data == null) + return Result.Fail("Settings are not specified"); + + var boringWordsPath = string.IsNullOrWhiteSpace(data.BoringWordsPath) + ? null + : data.BoringWordsPath; + + var settings = new TagCloudSettings( + data.Width, + data.Height, + data.FontFamily, + data.ColorScheme, + data.Algorithm, + data.MinFontSize, + data.MaxFontSize, + boringWordsPath); + return new TagCloudSettingsValidator().Validate(settings); + } } diff --git a/TagsCloudContainer.Core/Models/TagCloudSettingsDto.cs b/TagsCloudContainer.Core/Models/TagCloudSettingsDto.cs new file mode 100644 index 000000000..be5657447 --- /dev/null +++ b/TagsCloudContainer.Core/Models/TagCloudSettingsDto.cs @@ -0,0 +1,15 @@ +using TagsCloudContainer.Core.Infrastructure.Coloring; +using TagsCloudContainer.Core.Infrastructure.Layout; + +namespace TagsCloudContainer.Core.Models; +public class TagCloudSettingsDto +{ + public int Width { get; set; } = 800; + public int Height { get; set; } = 600; + public string FontFamily { get; set; } = "Arial"; + public ColorSchemeType ColorScheme { get; set; } = ColorSchemeType.Random; + public TagCloudAlgorithmType Algorithm { get; set; } = TagCloudAlgorithmType.Spiral; + public int MinFontSize { get; set; } = 10; + public int MaxFontSize { get; set; } = 50; + public string? BoringWordsPath { get; set; } = string.Empty; +} diff --git a/TagsCloudContainer.Core/Result.cs b/TagsCloudContainer.Core/Result.cs index 640be3879..9cf6a57f3 100644 --- a/TagsCloudContainer.Core/Result.cs +++ b/TagsCloudContainer.Core/Result.cs @@ -70,20 +70,6 @@ public static Result OfAction(Action f, string? error = null) } } - 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, Func> continuation)