Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions TagsCloudContainer.Console/CommandLineOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
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; }
}
44 changes: 44 additions & 0 deletions TagsCloudContainer.Console/ConsoleClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
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(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);
})
.Then(image => renderer.SaveToFile(image, outputFile))
.RefineError("Tag cloud generation failed");
}
private static Result<IReadOnlyList<string>> EnsureNotEmpty(IReadOnlyList<string> words)
{
return words.Count == 0
? Result.Fail<IReadOnlyList<string>>("No words to build a tag cloud")
: Result.Ok(words);
}
}
118 changes: 118 additions & 0 deletions TagsCloudContainer.Console/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
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 ValidateCommandLineOptions(options)
.Then(LoadSettings)
.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<CommandLineOptions> ValidateCommandLineOptions(CommandLineOptions o)
{
if (string.IsNullOrWhiteSpace(o.InputFile))
return Result.Fail<CommandLineOptions>("Input file is not specified");

return string.IsNullOrWhiteSpace(o.OutputFile)
? Result.Fail<CommandLineOptions>("Output file is not specified")
: Result.Ok(o);
}

static TagCloudSettingsDto CreateSettingsFromOptions(CommandLineOptions o)
{
return new TagCloudSettingsDto
{
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 TagCloudSettings.Create(CreateSettingsFromOptions(o));

if (!File.Exists(o.SettingsFile))
return Result.Fail<TagCloudSettings>($"Settings file not found: {o.SettingsFile}");

return Result.Of(() => File.ReadAllText(o.SettingsFile),
$"Failed to read settings file: {o.SettingsFile}")
.Then(DeserializeSettings)
.Then(settings => TagCloudSettings.Create(OverrideBoringWords(settings, o.BoringWordsFile)));

static Result<TagCloudSettingsDto> DeserializeSettings(string json)
{
try
{
var settings = JsonSerializer.Deserialize<TagCloudSettingsDto>(
json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
return settings == null
? Result.Fail<TagCloudSettingsDto>("Settings file is empty or invalid.")
: Result.Ok(settings);
}
catch (Exception e)
{
return Result.Fail<TagCloudSettingsDto>($"Failed to parse settings file: {e.Message}");
}
}
}

static TagCloudSettingsDto OverrideBoringWords(TagCloudSettingsDto s, string? path)
{
if (string.IsNullOrWhiteSpace(path))
return s;

return new TagCloudSettingsDto
{
Width = s.Width,
Height = s.Height,
FontFamily = s.FontFamily,
ColorScheme = s.ColorScheme,
MinFontSize = s.MinFontSize,
MaxFontSize = s.MaxFontSize,
Algorithm = s.Algorithm,
BoringWordsPath = path
};
}
14 changes: 14 additions & 0 deletions TagsCloudContainer.Console/TagsCloudContainer.Console.csproj
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>
Binary file added TagsCloudContainer.Core/.DS_Store
Binary file not shown.
56 changes: 56 additions & 0 deletions TagsCloudContainer.Core/DI/AutofacModule.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
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();
}
}
13 changes: 13 additions & 0 deletions TagsCloudContainer.Core/DI/CompositePreprocessor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
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));
}
}
Binary file not shown.
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,6 @@
namespace TagsCloudContainer.Core.Infrastructure.Coloring;
public enum ColorSchemeType
{
Random,
Gradient
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
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);
}
Original file line number Diff line number Diff line change
@@ -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];
}
}
Loading