-
Notifications
You must be signed in to change notification settings - Fork 240
Кроткая Александра #217
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Кроткая Александра #217
Changes from 4 commits
8e0452a
e00df84
cad8286
812771d
b4eb142
98945c3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| 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; | ||
|
|
||
| [Option("settings", HelpText = "Path to JSON settings file")] | ||
| public string? SettingsFile { get; init; } | ||
|
|
||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| using ResultOf; | ||
| 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 Result<None> GenerateTagCloud( | ||
| string inputFile, | ||
| string outputFile, | ||
| LayoutOptions options) | ||
| { | ||
| 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<IReadOnlyList<string>>("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"); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,135 @@ | ||
| 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<CommandLineOptions>(args) | ||
| .MapResult( | ||
| options => | ||
| { | ||
| return LoadSettings(options) | ||
| .Then<TagCloudSettings, None>(settings => | ||
| { | ||
| 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<CommandLineOptions>(); | ||
| builder.RegisterType<ConsoleClient>().SingleInstance(); | ||
|
|
||
| var container = builder.Build(); | ||
| return container.Resolve<ConsoleClient>(); | ||
| }, "Failed to initialize DI container") | ||
| .Then<ConsoleClient, None>(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; | ||
| }); | ||
|
|
||
|
|
||
| static Result<TagCloudSettings> ValidateOptions(CommandLineOptions o) | ||
| { | ||
| if (o.Width <= 0 || o.Height <= 0) | ||
| return Result.Fail<TagCloudSettings>("Invalid image size. Width and height must be positive"); | ||
|
||
|
|
||
| if (o.MinFontSize <= 0 || o.MaxFontSize <= 0) | ||
|
||
| return Result.Fail<TagCloudSettings>("Invalid font size. Min/Max must be positive"); | ||
|
|
||
| if (o.MinFontSize > o.MaxFontSize) | ||
| return Result.Fail<TagCloudSettings>("Invalid font size range. MinFontSize > MaxFontSize"); | ||
|
|
||
| if (string.IsNullOrWhiteSpace(o.InputFile)) | ||
| return Result.Fail<TagCloudSettings>("Input file is not specified"); | ||
|
|
||
| if (string.IsNullOrWhiteSpace(o.OutputFile)) | ||
| return Result.Fail<TagCloudSettings>("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<TagCloudSettings> LoadSettings(CommandLineOptions o) | ||
| { | ||
| if (string.IsNullOrWhiteSpace(o.SettingsFile)) | ||
| return ValidateOptions(o); // old CLI path | ||
|
|
||
| if (!File.Exists(o.SettingsFile)) | ||
| return Result.Fail<TagCloudSettings>($"Settings file not found: {o.SettingsFile}"); | ||
|
|
||
| return Result.Of(() => | ||
| { | ||
| var json = File.ReadAllText(o.SettingsFile); | ||
| var settings = JsonSerializer.Deserialize<TagCloudSettings>( | ||
| 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<TagCloudSettings> ValidateSettings(TagCloudSettings s) | ||
|
||
| { | ||
| if (s.Width <= 0 || s.Height <= 0) | ||
|
||
| return Result.Fail<TagCloudSettings>("Invalid image size. Width and height must be positive"); | ||
|
|
||
| if (s.MinFontSize <= 0 || s.MaxFontSize <= 0) | ||
| return Result.Fail<TagCloudSettings>("Invalid font size. Min/Max must be positive"); | ||
|
|
||
| if (s.MinFontSize > s.MaxFontSize) | ||
| return Result.Fail<TagCloudSettings>("Invalid font size range. MinFontSize > MaxFontSize"); | ||
|
|
||
| if (string.IsNullOrWhiteSpace(s.FontFamily)) | ||
| return Result.Fail<TagCloudSettings>("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 | ||
| }; | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| <Project Sdk="Microsoft.NET.Sdk"> | ||
|
|
||
| <PropertyGroup> | ||
| <OutputType>Exe</OutputType> | ||
| <TargetFramework>net8.0</TargetFramework> | ||
| <ImplicitUsings>enable</ImplicitUsings> | ||
| <Nullable>enable</Nullable> | ||
| </PropertyGroup> | ||
|
|
||
| <ItemGroup> | ||
| <ProjectReference Include="..\TagsCloudContainer.Core\TagsCloudContainer.Core.csproj" /> | ||
| </ItemGroup> | ||
|
|
||
| </Project> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<TagCloudSettings>(); | ||
|
|
||
| builder.RegisterType<TextFileReader>().As<IFileTextReader>(); | ||
| builder.RegisterType<DocxTextReader>().As<IFileTextReader>(); | ||
|
|
||
| builder.RegisterType<MultiFormatTextReader>() | ||
| .As<ITextReader>().SingleInstance(); | ||
|
|
||
| builder.Register<IBoringWordsProvider>(_ => | ||
| string.IsNullOrWhiteSpace(settings.BoringWordsPath) | ||
| ? new DefaultBoringWordsProvider() | ||
| : new FileBoringWordsProvider(settings.BoringWordsPath)) | ||
| .SingleInstance(); | ||
|
|
||
| builder.Register<ITextPreprocessor>(c => | ||
| { | ||
| var boringWords = c.Resolve<IBoringWordsProvider>(); | ||
| return new CompositePreprocessor([ | ||
| new LowerCaseNormalizer(), | ||
| new BoringWordsFilter(boringWords) | ||
| ]); | ||
| }).SingleInstance(); | ||
|
|
||
| builder.RegisterType<WordFrequencyAnalyzer>().As<IWordFrequencyAnalyzer>().SingleInstance(); | ||
|
|
||
| builder.Register<IFontSizeCalculator>(_ | ||
| => new LinearFontSizeCalculator(settings.MinFontSize, settings.MaxFontSize)).SingleInstance(); | ||
|
|
||
| builder.RegisterType<RandomColorScheme>() | ||
| .Keyed<IColorScheme>(ColorSchemeType.Random) | ||
| .SingleInstance(); | ||
|
|
||
| builder.Register(_ => new GradientColorScheme(SKColors.Blue, SKColors.LightBlue)) | ||
| .Keyed<IColorScheme>(ColorSchemeType.Gradient) | ||
| .SingleInstance(); | ||
|
|
||
| builder.Register<IColorScheme>(ctx => | ||
| ctx.ResolveKeyed<IColorScheme>(settings.ColorScheme)) | ||
| .SingleInstance(); | ||
|
|
||
| builder.RegisterType<SpiralTagCloudAlgorithm>() | ||
| .Keyed<ITagCloudAlgorithm>(TagCloudAlgorithmType.Spiral) | ||
| .SingleInstance(); | ||
|
|
||
| builder.RegisterType<TightSpiralTagCloudAlgorithm>() | ||
| .Keyed<ITagCloudAlgorithm>(TagCloudAlgorithmType.Tight) | ||
| .SingleInstance(); | ||
|
|
||
| builder.Register<ITagCloudAlgorithm>(ctx => | ||
| ctx.ResolveKeyed<ITagCloudAlgorithm>(settings.Algorithm)) | ||
| .SingleInstance(); | ||
|
|
||
| builder.RegisterType<PngRenderer>().As<ITagCloudRenderer>().SingleInstance(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| using ResultOf; | ||
| using TagsCloudContainer.Core.Infrastructure.Preprocessing; | ||
|
|
||
| namespace TagsCloudContainer.Core.DI; | ||
| public class CompositePreprocessor(IEnumerable<ITextPreprocessor> preprocessors) : ITextPreprocessor | ||
| { | ||
| public Result<IReadOnlyList<string>> Process(IEnumerable<string> words) | ||
| { | ||
| var current = Result.Ok<IReadOnlyList<string>>(words.ToArray()); | ||
| return preprocessors.Aggregate(current, (current1, preprocessor) | ||
| => current1.Then(preprocessor.Process)); | ||
| } | ||
|
|
||
|
|
||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| using ResultOf; | ||
| using TagsCloudContainer.Core.Models; | ||
|
|
||
| namespace TagsCloudContainer.Core.Infrastructure.Analysis; | ||
|
|
||
| public interface IWordFrequencyAnalyzer | ||
| { | ||
| Result<IReadOnlyList<WordFrequency>> Analyze(IEnumerable<string> words); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| using ResultOf; | ||
| using TagsCloudContainer.Core.Models; | ||
|
|
||
| namespace TagsCloudContainer.Core.Infrastructure.Analysis; | ||
| public class WordFrequencyAnalyzer : IWordFrequencyAnalyzer | ||
| { | ||
| public Result<IReadOnlyList<WordFrequency>> Analyze(IEnumerable<string> words) | ||
| { | ||
| var frequencies = words | ||
| .GroupBy(word => word) | ||
| .Select(group | ||
| => new WordFrequency(group.Key, group.Count())) | ||
| .OrderByDescending(wf => wf.Frequency).ToArray(); | ||
| return Result.Ok<IReadOnlyList<WordFrequency>>(frequencies); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| namespace TagsCloudContainer.Core.Infrastructure.Coloring; | ||
|
|
||
| public enum ColorSchemeType | ||
| { | ||
| Random, | ||
| Gradient | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| using SkiaSharp; | ||
|
|
||
| namespace TagsCloudContainer.Core.Infrastructure.Coloring; | ||
| public interface IColorScheme | ||
| { | ||
| SKColor GetColor(int seed); | ||
| } |
This comment was marked as resolved.
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Поняла про что ты, сам rider мне подсказал как можно сократить)