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
31 changes: 31 additions & 0 deletions TagsCloudContainer/CircularCloudLayouter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using System.Drawing;
using TagsCloudContainer.Interfaces;

namespace TagsCloudContainer;

public class CircularCloudLayouter(IPointGenerator pointGenerator): ITagCloudLayouter
{
private readonly List<Rectangle> _rectangles = new();
private readonly Grid _grid = new();
public Result<Rectangle> PutNextRectangle(Size rectangleSize)
{
if (rectangleSize.Width <= 0 || rectangleSize.Height <= 0)
return Result.Fail<Rectangle>("Размеры прямоугольника должны быть положительными.");

Rectangle newRectangle;
do
{
var point = pointGenerator.GetNextPoint();
var location = new Point(
point.X - rectangleSize.Width / 2,
point.Y - rectangleSize.Height / 2);
newRectangle = new Rectangle(location, rectangleSize);
} while (_grid.IsIntersecting(newRectangle));

_grid.AddRectangle(newRectangle);
_rectangles.Add(newRectangle);
return newRectangle;
}

public IEnumerable<Rectangle> GetRectangles() => _rectangles;
}
70 changes: 70 additions & 0 deletions TagsCloudContainer/CloudImageRenderer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
using System.Drawing;
using System.Drawing.Imaging;
using TagsCloudContainer.Interfaces;
using TagsCloudContainer.Options;

namespace TagsCloudContainer;

public class CloudImageRenderer : ITagCloudRenderer
{
private readonly Random random = new();
private const float MinFontSize = 12f;
private const float MaxFontSize = 72f;
private const float FrequencyMultiplier = 2f;

public Result<None> Render(IEnumerable<Tag> tags, string outputFilePath, RenderingOptions options)
{
var checkResult = CheckFitSize(tags, options);
if (!checkResult.IsSuccess)
{
return checkResult;
}

return Result.OfAction(() =>
{
using var bitmap = new Bitmap(options.ImageSize.Width, options.ImageSize.Height);
using var graphics = Graphics.FromImage(bitmap);

graphics.Clear(options.BackgroundColor);
graphics.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAlias;

foreach (var tag in tags)
{
var fontSize = Math.Max(MinFontSize, Math.Min(MaxFontSize, tag.Frequency * FrequencyMultiplier));
using var font = new Font(options.Font, fontSize, FontStyle.Bold);

var color = options.WordColors[random.Next(options.WordColors.Length)];
using var brush = new SolidBrush(color);

graphics.DrawString(
tag.Word,
font,
brush,
tag.Rectangle.Location
);
}

bitmap.Save(outputFilePath, ImageFormat.Png);
}, $"Ошибка при сохранении результата в файл: {outputFilePath}");
}

private static Result<None> CheckFitSize(IEnumerable<Tag> tags, RenderingOptions options)
{
if (!tags.Any())
return Result.Ok();

var minX = tags.Min(t => t.Rectangle.Left);
var minY = tags.Min(t => t.Rectangle.Top);
var maxX = tags.Max(t => t.Rectangle.Right);
var maxY = tags.Max(t => t.Rectangle.Bottom);

if (minX < 0 || minY < 0 ||
maxX > options.ImageSize.Width ||
maxY > options.ImageSize.Height)
{
return Result.Fail<None>("Облако тегов вышло за пределы изображения.");
}

return Result.Ok();
}
}
76 changes: 76 additions & 0 deletions TagsCloudContainer/DependencyInjectionConfig.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
using System.Drawing;
using Autofac;
using TagsCloudContainer.DocumentReaders;
using TagsCloudContainer.Interfaces;
using TagsCloudContainer.Options;
using TagsCloudContainer.PointGenerators;

namespace TagsCloudContainer;

public static class DependencyInjectionConfig
{
public static IContainer BuildContainer(Options.Options? options = null)
{
var builder = new ContainerBuilder();

builder.Register(c => new RenderingOptions())
.AsSelf()
.SingleInstance();

var boringWords = "";
if (options is not null)
{
boringWords = options.BoringWordsFilePath;
}

builder.RegisterType<WordProcessor>()
.As<IWordProcessor>()
.SingleInstance()
.WithParameter("filepath",
boringWords);

builder.RegisterType<WordDocumentReader>().As<IDocumentReader>().SingleInstance();
builder.RegisterType<TxtReader>().As<IDocumentReader>().SingleInstance();

builder.RegisterType<DocumentReader>().AsSelf().SingleInstance();

builder.RegisterType<LogarithmicScaling>().As<ITextSizeCalculator>().SingleInstance();

builder.RegisterType<WordFrequencyAnalyzer>()
.As<IWordFrequencyAnalyzer>()
.SingleInstance();

builder.RegisterType<CircularCloudLayouter>()
.As<ITagCloudLayouter>()
.SingleInstance();

builder.RegisterType<SpiralGenerator>()
.As<IPointGenerator>()
.SingleInstance()
.WithParameter("center", new Point(500, 500));

builder.RegisterType<LinearSpiral>()
.Named<IPointGenerator>("linear");

builder.RegisterType<CloudImageRenderer>()
.As<ITagCloudRenderer>()
.SingleInstance();

builder.RegisterType<TagCloudGenerator>()
.As<ITagCloudGenerator>()
.SingleInstance();

builder.Register<ITagCloudGenerator>(c =>
new TagCloudGenerator(
c.Resolve<IWordProcessor>(),
c.Resolve<ITagCloudLayouter>(),
c.Resolve<ITagCloudRenderer>(),
c.Resolve<DocumentReader>(),
c.Resolve<IWordFrequencyAnalyzer>(),
c.Resolve<ITextSizeCalculator>()
))
.SingleInstance();

return builder.Build();
}
}
21 changes: 21 additions & 0 deletions TagsCloudContainer/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
FROM mcr.microsoft.com/dotnet/runtime:8.0 AS base
USER $APP_UID
WORKDIR /app

FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY ["TagsCloudContainer/TagsCloudContainer.csproj", "TagsCloudContainer/"]
RUN dotnet restore "TagsCloudContainer/TagsCloudContainer.csproj"
COPY . .
WORKDIR "/src/TagsCloudContainer"
RUN dotnet build "TagsCloudContainer.csproj" -c $BUILD_CONFIGURATION -o /app/build

FROM build AS publish
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "TagsCloudContainer.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "TagsCloudContainer.dll"]
28 changes: 28 additions & 0 deletions TagsCloudContainer/DocumentReaders/DocumentReader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using TagsCloudContainer.Interfaces;

namespace TagsCloudContainer.DocumentReaders;

public class DocumentReader: IDocumentReader
{
private readonly Dictionary<string, IDocumentReader> _readers;
public string[] SupportedDocumentExtensions => _readers.Keys.ToArray();
public DocumentReader(IEnumerable<IDocumentReader> readers)
{
_readers = readers
.SelectMany(reader => reader.SupportedDocumentExtensions.Select(ext => new { ext, reader }))
.ToDictionary(x => x.ext, x => x.reader);
}
public Result<string[]?> ReadDocument(string filePath)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Почему string[]? ?
Когда мы тут без ошибки null вернём?

{
if (!File.Exists(filePath))
return Result.Fail<string[]>($"Файл не найден или недоступен: {filePath}");

var extension = Path.GetExtension(filePath).ToLower();
if (!_readers.TryGetValue(extension, out var reader))
return Result.Fail<string[]>($"Формат файла {extension} не поддерживается.");

return reader
.ReadDocument(filePath)
.RefineError($"Не удалось прочитать документ: {filePath}");
}
}
13 changes: 13 additions & 0 deletions TagsCloudContainer/DocumentReaders/TxtReader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using TagsCloudContainer.Interfaces;

namespace TagsCloudContainer.DocumentReaders;

public class TxtReader : IDocumentReader
{
public string[] SupportedDocumentExtensions => [".txt"];

public Result<string[]?> ReadDocument(string filePath)
{
return File.ReadAllLines(filePath);
}
}
19 changes: 19 additions & 0 deletions TagsCloudContainer/DocumentReaders/WordDocumentReader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using DocumentFormat.OpenXml.Packaging;
using TagsCloudContainer.Interfaces;

namespace TagsCloudContainer.DocumentReaders;

public class WordDocumentReader : IDocumentReader
{
public string[] SupportedDocumentExtensions => [".doc", ".docx"];

public Result<string[]?> ReadDocument(string filePath)
{
using var doc = WordprocessingDocument.Open(filePath, false);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

А тут не будет ошибки, если документа по пути не найдется?

var body = doc.MainDocumentPart?.Document.Body;
if (body == null) return Array.Empty<string>();

var text = body.InnerText;
return text.Split(new[] { ' ', '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
}
}
13 changes: 13 additions & 0 deletions TagsCloudContainer/Extensions/PointExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System.Drawing;

namespace TagsCloudContainer.Extensions;

public static class PointExtensions
{
public static double DistanceFromCenter(this Point point, Point center)
{
var dx = point.X - center.X;
var dy = point.Y - center.Y;
return Math.Sqrt(dx * dx + dy * dy);
}
}
64 changes: 64 additions & 0 deletions TagsCloudContainer/Grid.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
using System.Drawing;

namespace TagsCloudContainer;

public class Grid
{
private readonly int cellSize;
private readonly Dictionary<Point, List<Rectangle>> cells = new();

public Grid(int cellSize = 50)
{
this.cellSize = cellSize;
}
public void AddRectangle(Rectangle rectangle)
{
var cellsToUpdate = GetCellsCoveredByRectangle(rectangle);

foreach (var cell in cellsToUpdate)
{
if (!cells.ContainsKey(cell))
{
cells[cell] = new List<Rectangle>();
}

cells[cell].Add(rectangle);
}
}

public bool IsIntersecting(Rectangle rectangle)
{
var cellsToCheck = GetCellsCoveredByRectangle(rectangle);

foreach (var cell in cellsToCheck)
{
if (!cells.TryGetValue(cell, out var rectanglesInCell))
{
continue;
}

if (rectanglesInCell.Any(r => r.IntersectsWith(rectangle)))
{
return true;
}
}

return false;
}

private IEnumerable<Point> GetCellsCoveredByRectangle(Rectangle rectangle)
{
var startX = rectangle.Left / cellSize;
var endX = rectangle.Right / cellSize;
var startY = rectangle.Top / cellSize;
var endY = rectangle.Bottom / cellSize;

for (var x = startX; x <= endX; x++)
{
for (var y = startY; y <= endY; y++)
{
yield return new Point(x, y);
}
}
}
}
7 changes: 7 additions & 0 deletions TagsCloudContainer/Interfaces/IDocumentReader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace TagsCloudContainer.Interfaces;

public interface IDocumentReader
{
string[] SupportedDocumentExtensions { get; }
Result<string[]?> ReadDocument(string filePath);
}
8 changes: 8 additions & 0 deletions TagsCloudContainer/Interfaces/IPointGenerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using System.Drawing;

namespace TagsCloudContainer.Interfaces;

public interface IPointGenerator
{
Point GetNextPoint();
}
8 changes: 8 additions & 0 deletions TagsCloudContainer/Interfaces/ITagCloudGenerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using TagsCloudContainer.Options;

namespace TagsCloudContainer.Interfaces;

public interface ITagCloudGenerator
{
Result<None> GenerateCloud(string inputFilePath, string outputFilePath, RenderingOptions options);
}
9 changes: 9 additions & 0 deletions TagsCloudContainer/Interfaces/ITagCloudLayouter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using System.Drawing;

namespace TagsCloudContainer.Interfaces;

public interface ITagCloudLayouter
{
Result<Rectangle> PutNextRectangle(Size rectangleSize);
IEnumerable<Rectangle> GetRectangles();
}
Loading