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
6 changes: 6 additions & 0 deletions TagCloudResult/ResultTools/None.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace ResultTools;

public class None
{

}
62 changes: 62 additions & 0 deletions TagCloudResult/ResultTools/Result.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
namespace ResultTools;

public class Result<T>
Copy link

@GlazProject GlazProject Jan 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Если пытаться избавиться от Result<None>, то можно посмотреть как сделаны Task и Task<T>. Тогда бы ошибка и IsSuccess вынеслись в Result, а Result<T>:Result принёс ещё и Value. Но в этом случае придётся немного переписать (дописать) методы расширений. Тогда пропадёт такая особенность, что в метод передаётся None при вызове Then(none =>)

{
private T? value;
public T? Value
{
get
{
if (IsSuccess)
return value;
throw new InvalidOperationException(Error);
}

private init => this.value = value;
}

public string? Error { get; }
public bool IsSuccess => Error == null;

public Result(string? error, T? value = default)
{
Error = error;
Value = value;
}
}

public static class Result
{
public static Result<T> Ok<T>(T value) => new(null, value);

public static Result<None> Ok() => new(null);

public static Result<T> AsResult<T>(this T value) => Ok(value);

public static Result<T> Fail<T>(string e) => new(e);

public static Result<T> Of<T>(Func<T> f, string? error = null)
{
try
{
return Ok(f());
}
catch (Exception e)
{
return Fail<T>(error ?? e.Message);
}
}

public static Result<None> OfAction(Action f, string? error = null)
{
try
{
f();
return Ok();
}
catch (Exception e)
{
return Fail<None>(error ?? e.Message);
}
}
}
45 changes: 45 additions & 0 deletions TagCloudResult/ResultTools/ResultExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
namespace ResultTools;

public static class ResultExtensions
{
public static Result<TOutput> Then<TInput, TOutput>(
this Result<TInput> input,
Func<TInput, TOutput> continuation)
=> input.Then(inp => Result.Of(() => continuation(inp)));

public static Result<None> Then<TInput>(
this Result<TInput> input,
Action<TInput> continuation)
=> input.Then(inp => Result.OfAction(() => continuation(inp)));

public static Result<TOutput> Then<TInput, TOutput>(
this Result<TInput> input,
Func<TInput, Result<TOutput>> continuation)
=> input.IsSuccess
? continuation(input.Value!)
: Result.Fail<TOutput>(input.Error!);

public static Result<TOutput> Then<TInput, TOutput>(this Result<TInput> input, Result<TOutput> value)
=> input.Then(_ => value);

public static Result<TInput> OnFail<TInput>(
this Result<TInput> input,
Action<string> handleError)
{
if (!input.IsSuccess)
handleError(input.Error!);
return input;
}

public static Result<TInput> ReplaceError<TInput>(
this Result<TInput> input,
Func<string, string> replaceError)
=> input.IsSuccess
? input
: Result.Fail<TInput>(replaceError(input.Error!));

public static Result<TInput> RefineError<TInput>(
this Result<TInput> input,
string error)
=> input.ReplaceError(err => $"{error}. {err}");
}
9 changes: 9 additions & 0 deletions TagCloudResult/ResultTools/ResultTools.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

</Project>
46 changes: 46 additions & 0 deletions TagCloudResult/TagCloud.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.9.34728.123
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TagCloud", "TagCloud\TagCloud.csproj", "{2AF38A2D-EE49-4C1D-A38F-7B524AF31ACA}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TagCloudTests", "TagCloudTests\TagCloudTests.csproj", "{68CB9437-2F08-4A23-A165-ECB72F1C0E07}"
ProjectSection(ProjectDependencies) = postProject
{F41DEF5A-5A77-4DD8-8987-05AAF41B37F7} = {F41DEF5A-5A77-4DD8-8987-05AAF41B37F7}
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TagCloudClients", "TagCloudClients\TagCloudClients.csproj", "{F41DEF5A-5A77-4DD8-8987-05AAF41B37F7}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ResultTools", "ResultTools\ResultTools.csproj", "{08119452-50F7-4CF5-A28F-59122A12EECF}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{2AF38A2D-EE49-4C1D-A38F-7B524AF31ACA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2AF38A2D-EE49-4C1D-A38F-7B524AF31ACA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2AF38A2D-EE49-4C1D-A38F-7B524AF31ACA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2AF38A2D-EE49-4C1D-A38F-7B524AF31ACA}.Release|Any CPU.Build.0 = Release|Any CPU
{68CB9437-2F08-4A23-A165-ECB72F1C0E07}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{68CB9437-2F08-4A23-A165-ECB72F1C0E07}.Debug|Any CPU.Build.0 = Debug|Any CPU
{68CB9437-2F08-4A23-A165-ECB72F1C0E07}.Release|Any CPU.ActiveCfg = Release|Any CPU
{68CB9437-2F08-4A23-A165-ECB72F1C0E07}.Release|Any CPU.Build.0 = Release|Any CPU
{F41DEF5A-5A77-4DD8-8987-05AAF41B37F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F41DEF5A-5A77-4DD8-8987-05AAF41B37F7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F41DEF5A-5A77-4DD8-8987-05AAF41B37F7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F41DEF5A-5A77-4DD8-8987-05AAF41B37F7}.Release|Any CPU.Build.0 = Release|Any CPU
{08119452-50F7-4CF5-A28F-59122A12EECF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{08119452-50F7-4CF5-A28F-59122A12EECF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{08119452-50F7-4CF5-A28F-59122A12EECF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{08119452-50F7-4CF5-A28F-59122A12EECF}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {697D441B-1F34-4177-90F6-6EB0176AF803}
EndGlobalSection
EndGlobal
69 changes: 69 additions & 0 deletions TagCloudResult/TagCloud/BitmapGenerators/BitmapGenerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
using System.Diagnostics.CodeAnalysis;
using System.Drawing;
using ResultTools;
using TagCloud.CloudLayouter;
using TagCloud.SettingsProviders;

namespace TagCloud.BitmapGenerators;

[SuppressMessage("Interoperability", "CA1416:Проверка совместимости платформы")]
public class BitmapGenerator: IBitmapGenerator
{
private readonly ISettingsProvider<BitmapGeneratorSettings> settingsProvider;
private readonly ICloudLayouter layouter;

public BitmapGenerator(ICloudLayouter layouter, ISettingsProvider<BitmapGeneratorSettings> settingsProvider)
{
this.settingsProvider = settingsProvider;
this.layouter = layouter;
}

public Result<Bitmap> GenerateBitmapFromWords(IEnumerable<CloudWord> words)
{
var padding = 1.5f;
var settings = settingsProvider.GetSettings();

var bitmap = settings.ImageSize is {Width: > 0, Height: > 0}
? new Bitmap(settings.ImageSize.Width, settings.ImageSize.Height).AsResult()

: Result.Fail<Bitmap>("Bitmap size must be grater than zero");

return bitmap
.Then(b =>
{
using var graphics = Graphics.FromImage(b);

graphics.Clear(settings.BackgroundColor);

return DrawWords(words, graphics, settings)
.Then(bitmap);
});
}

private Result<None> DrawWords(IEnumerable<CloudWord> words, Graphics graphics, BitmapGeneratorSettings settings)
{
const float padding = 1.5f;
using var brush = new SolidBrush(settings.WordColor);
foreach (var word in words)
{
if (word.Word is null)
return Result.Fail<None>($"Word {word.Word} has no word");
using var font = new Font(settings.FontFamily, word.FontSize);
var size = graphics.MeasureString(word.Word, font);
var result = layouter.PutNextRectangle(size.ToSize())
.Then(p => IsRectangleInBounds(p, settings.ImageSize)
? p.AsResult()
: Result.Fail<Rectangle>("Point is out of bounds"))
.Then(p => new PointF(p.X + padding, p.Y + padding))
.Then(p => graphics.DrawString(word.Word, font, brush, p));
if (!result.IsSuccess)
return result;
}

return Result.Ok();
}

private bool IsRectangleInBounds(Rectangle rectangle, Size size)
=> rectangle.Left > 0 || rectangle.Top < 0 || rectangle.Right < size.Width || rectangle.Bottom > size.Height;
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using System.Drawing;

namespace TagCloud.BitmapGenerators;

public record BitmapGeneratorSettings
{
public Size ImageSize { get; private set; }
public Color BackgroundColor { get; private set; }
public Color WordColor { get; private set; }
public FontFamily FontFamily { get; private set; }

public BitmapGeneratorSettings()
: this(new Size(1080, 1920), Color.White, Color.Black, new FontFamily("Arial"))
{

}

public BitmapGeneratorSettings(
Size imageSize,
Color backgroudColor,
Color wordColor,
FontFamily fontFamily)
{
ImageSize = imageSize;
BackgroundColor = backgroudColor;
WordColor = wordColor;
FontFamily = fontFamily;
}
}
3 changes: 3 additions & 0 deletions TagCloudResult/TagCloud/BitmapGenerators/CloudWord.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace TagCloud.BitmapGenerators;

public record CloudWord(string Word, int FontSize);
9 changes: 9 additions & 0 deletions TagCloudResult/TagCloud/BitmapGenerators/IBitmapGenerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using System.Drawing;
using ResultTools;

namespace TagCloud.BitmapGenerators;

public interface IBitmapGenerator
{
Result<Bitmap> GenerateBitmapFromWords(IEnumerable<CloudWord> words);
}
25 changes: 25 additions & 0 deletions TagCloudResult/TagCloud/CloudImageSavers/CloudImageSaver.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System.Drawing;
using TagCloud.SettingsProviders;

namespace TagCloud.CloudImageSavers;

#pragma warning disable CA1416
public class CloudImageSaver : ICloudImageSaver
{
private readonly ISettingsProvider<SaveSettings> settingsProvider;

public CloudImageSaver(ISettingsProvider<SaveSettings> settingsProvider)
{
this.settingsProvider = settingsProvider;
}

public string Save(Bitmap image)
{
var settings = settingsProvider.GetSettings();
var filename = $"{settings.FileName}.{settings.Format.ToString().ToLower()}";

image.Save(filename, settings.Format);
return Path.Combine(Directory.GetCurrentDirectory(), filename);
}
}
#pragma warning restore CA1416
8 changes: 8 additions & 0 deletions TagCloudResult/TagCloud/CloudImageSavers/ICloudImageSaver.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using System.Drawing;

namespace TagCloud.CloudImageSavers;

public interface ICloudImageSaver
{
string Save(Bitmap image);
}
20 changes: 20 additions & 0 deletions TagCloudResult/TagCloud/CloudImageSavers/SaveSettings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System.Drawing.Imaging;

namespace TagCloud.CloudImageSavers;

public record SaveSettings
{
public string FileName { get; private set; }
public ImageFormat Format { get; private set; }

public SaveSettings() : this("output", ImageFormat.Png)
{

}

public SaveSettings(string fileName, ImageFormat format)
{
FileName = fileName;
Format = format;
}
}
52 changes: 52 additions & 0 deletions TagCloudResult/TagCloud/CloudLayouter/CircularCloudLayouter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using System.Drawing;
using ResultTools;
using TagCloud.CloudLayouter.PositionGenerator;

namespace TagCloud.CloudLayouter;
public class CircularCloudLayouter : ICloudLayouter
{
private readonly List<Rectangle> rectangles;
private readonly IEnumerator<Result<Point>> pointEnumerator;

public CircularCloudLayouter(IPositionGenerator generator)
{
rectangles = new();
pointEnumerator = generator.GetPositions().GetEnumerator();
}

public List<Rectangle> GetRectangles() => rectangles;

public Result<Rectangle> PutNextRectangle(Size rectangleSize)
{
if (rectangleSize.Width <= 0 || rectangleSize.Height <= 0)
return Result.Fail<Rectangle>(
$"{nameof(rectangleSize)} должен иметь высоту и ширину больше нуля," +
$" передано ({rectangleSize.Width} {rectangleSize.Height})");

Result<Rectangle> rectangle;

do
{
rectangle = PutRectangleInNextPosition(rectangleSize);
if (!rectangle.IsSuccess)
return rectangle;
}
while (rectangles.Any(r => r.IntersectsWith(rectangle.Value)));

rectangles.Add(rectangle.Value);
return rectangle;
}

private Result<Rectangle> PutRectangleInNextPosition(Size rectagleSize)
{
if (!pointEnumerator.MoveNext())
return Result.Fail<Rectangle>("Cannot get next rectangle");
var centerOfRectangle = pointEnumerator.Current;
return centerOfRectangle
.Then(c => GetPositionFromCenter(c, rectagleSize))
.Then(c => new Rectangle(c, rectagleSize));
}

private Point GetPositionFromCenter(Point center, Size size) =>
new(center.X - size.Width / 2, center.Y - size.Height / 2);
}
Loading