This commit is contained in:
Simon Gruber
2024-01-08 16:21:10 +01:00
parent f3768348e9
commit b17044f959
51 changed files with 263 additions and 274 deletions
@@ -0,0 +1,59 @@
using ReportGeneration.Interface;
using System;
using System.IO;
using System.Text;
namespace ReportGeneration.Abstract;
public abstract class DocumentGeneratorBase : StreamWriterBase, IDocumentGenerator
{
/// <inheritdoc />
protected DocumentGeneratorBase() { }
/// <inheritdoc />
protected DocumentGeneratorBase(Stream stream) : base(stream) { }
/// <inheritdoc />
protected DocumentGeneratorBase(string filePath) : base(File.Open(filePath, FileMode.Create,
FileAccess.Write))
{ }
/// <inheritdoc />
protected DocumentGeneratorBase(Stream stream, Encoding encoding) : base(stream, encoding) { }
#region Writing
/// <inheritdoc />
public virtual IDocumentGenerator Append(string? text = default)
{
Write(text);
return this;
}
/// <inheritdoc />
public virtual IDocumentGenerator AppendLine(string? text = default)
{
WriteLine(text);
return this;
}
/// <inheritdoc />
public abstract IDocumentGenerator AppendHeading(int level, string text);
/// <inheritdoc />
public abstract IDocumentGenerator AppendParagraph(string? text = default);
/// <inheritdoc />
public IDocumentGenerator AppendTable(int columns, Action<ITableGenerator> table)
{
Write(() => MakeTable(columns, new MemoryStream()), table);
return this;
}
protected abstract ITableGenerator MakeTable(int columns, Stream stream);
#endregion
/// <inheritdoc />
public abstract string FormatImage(string path, IBounds? bounds = default);
}
@@ -0,0 +1,44 @@
using ReportGeneration.Interface;
namespace ReportGeneration.Abstract.Model;
public struct Bounds : IBounds
{
/// <inheritdoc />
public string Unit => "px";
/// <inheritdoc />
public int? MinWidth { get; set; } = null;
/// <inheritdoc />
public int? MinHeight { get; set; } = null;
/// <inheritdoc />
public int? MaxWidth { get; set; } = null;
/// <inheritdoc />
public int? MaxHeight { get; set; } = null;
/// <inheritdoc />
public int? Width { get; set; } = null;
/// <inheritdoc />
public int? Height { get; set; } = null;
public Bounds() { }
public Bounds(int? size)
{
Width = size;
Height = size;
}
public Bounds(int? min, int? max, int? size = null) : this(size)
{
MinWidth = min;
MinHeight = min;
MaxWidth = max;
MaxHeight = max;
}
}
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
<IncludeSymbols>True</IncludeSymbols>
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\ReportGeneration.Interface\ReportGeneration.Interface.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,200 @@
using ReportGeneration.Interface;
using System;
using System.IO;
using System.Text;
namespace ReportGeneration.Abstract;
public abstract class StreamWriterBase : IStreamWriter
{
private bool _isOpen;
private bool _isClosed;
/// <summary>
/// Underlying <see cref="Stream"/>
/// </summary>
private Stream Stream { get; }
/// <summary>
/// Internal <see cref="StreamWriter"/> for generating the output <see cref="string"/>
/// </summary>
protected TextWriter Writer { get; }
/// <summary>
/// Constructor; Configures the
/// <see cref="StreamWriterBase"/> to write to the memory
/// </summary>
protected StreamWriterBase() : this(new MemoryStream()) { }
/// <inheritdoc cref="StreamWriterBase(System.IO.Stream, Encoding)"/>
protected StreamWriterBase(Stream stream) : this(stream, Encoding.UTF8) { }
/// <summary>
/// Constructor; Configures the <see cref="StreamWriterBase"/>
/// to write to the specified <paramref name="stream"/>
/// </summary>
/// <param name="stream">The <see cref="Stream"/> to write to</param>
/// <param name="encoding">Text <see cref="Encoding"/> of the written data</param>
protected StreamWriterBase(Stream stream, Encoding encoding)
{
Stream = stream;
Writer = new StreamWriter(stream, encoding);
}
#region Control
public void Open()
{
if (_isOpen)
{
throw new InvalidOperationException($"{GetType()} has already been opened");
}
if (_isClosed)
{
throw new InvalidOperationException($"Cannot call open on a closed {GetType()}");
}
_isOpen = true;
OnOpen();
}
public void Close()
{
if (_isClosed)
{
throw new InvalidOperationException($"{GetType()} has already been closed");
}
if (!_isOpen)
{
throw new InvalidOperationException($"{GetType()} has never been opened");
}
_isClosed = true;
OnClose();
Writer.Flush();
}
/// <summary>
/// Called once the internal writer has been initialized
/// and the <see cref="Stream"/> is ready for writing
/// </summary>
protected virtual void OnOpen() { }
/// <summary>
/// Called once the document is about to be closed
/// </summary>
protected virtual void OnClose() { }
#endregion
#region Reading
/// <inheritdoc />
public StreamReader Read()
{
Writer.Flush();
Stream.Seek(0, SeekOrigin.Begin);
return new StreamReader(Stream, Writer.Encoding);
}
#endregion
#region Writing
public IStreamWriter Write(IStreamWriter writer)
{
using var reader = writer.Read();
return Write(reader);
}
public IStreamWriter Write(StreamReader reader)
{
int bytesRead;
char[] buffer = new char[4096];
while ((bytesRead = reader.Read(buffer, 0, buffer.Length)) > 0)
{
Writer.Write(buffer, 0, bytesRead);
}
return this;
}
public IStreamWriter Write(string? text = default)
{
Writer.Write(text);
return this;
}
public IStreamWriter WriteLine(string? text = default)
{
Writer.WriteLine(text);
return this;
}
/// <summary>
/// Creates and configures the <typeparamref name="T"/> using the given functions
/// </summary>
/// <typeparam name="T">Type of the <see cref="IStreamWriter"/></typeparam>
/// <param name="makeFunc">Function used to generate the <typeparamref name="T"/></param>
/// <param name="configFunc">Function used to configure the <typeparamref name="T"/></param>
/// <returns></returns>
public IStreamWriter Write<T>(Func<T> makeFunc, Action<T> configFunc)
where T : IStreamWriter
{
using var writer = makeFunc();
writer.Open();
configFunc(writer);
writer.Close();
Write(writer);
return this;
}
#endregion
#region IDisposable
private bool _disposed;
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
try
{
// Close document
Close();
}
catch (InvalidOperationException)
{
// ignore
}
finally
{
// Dispose stream
Stream.Dispose();
}
}
}
/// <inheritdoc />
public void Dispose()
{
if (_disposed)
{
return;
}
_disposed = true;
Dispose(true);
GC.SuppressFinalize(this);
}
#endregion
}
@@ -0,0 +1,70 @@
using ReportGeneration.Interface;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
namespace ReportGeneration.Abstract;
public abstract class TableGeneratorBase : StreamWriterBase, ITableGenerator
{
/// <inheritdoc />
public int Columns { get; }
/// <inheritdoc />
protected TableGeneratorBase(int columns) =>
Columns = columns;
/// <inheritdoc />
protected TableGeneratorBase(int columns, Stream stream)
: base(stream) => Columns = columns;
/// <inheritdoc />
protected TableGeneratorBase(int columns, Stream stream, Encoding encoding)
: base(stream, encoding) => Columns = columns;
#region Header
/// <inheritdoc />
public virtual ITableGenerator AppendHeader(string content) =>
AppendHeader(Enumerable.Range(0, Columns).Select(_ => content));
/// <inheritdoc />
public abstract ITableGenerator AppendHeader(IEnumerable<string> row);
/// <inheritdoc />
public virtual ITableGenerator AppendHeader(IEnumerable<IEnumerable<string>> rows)
{
foreach (var row in rows)
{
AppendHeader(row);
}
return this;
}
#endregion
#region Row
/// <inheritdoc />
public virtual ITableGenerator AppendRow(string content) =>
AppendRow(Enumerable.Range(0, Columns).Select(_ => content));
/// <inheritdoc />
public abstract ITableGenerator AppendRow(IEnumerable<string> row);
/// <inheritdoc />
public virtual ITableGenerator AppendRows(IEnumerable<IEnumerable<string>> rows)
{
foreach (var row in rows)
{
AppendRow(row);
}
return this;
}
#endregion
}
@@ -0,0 +1,42 @@
using ReportGeneration.Abstract;
using System.IO;
using System.Text;
namespace ReportGeneration.Generators;
internal class CollapsibleHtmlTableGenerator : HtmlTableGenerator
{
public string DetailsClass { get; init; }
public string Summary { get; init; } = "Show table";
/// <inheritdoc />
public CollapsibleHtmlTableGenerator(int columns)
: base(columns) { }
/// <inheritdoc />
public CollapsibleHtmlTableGenerator(int columns, Stream stream)
: base(columns, stream) { }
/// <inheritdoc />
public CollapsibleHtmlTableGenerator(int columns, Stream stream, Encoding encoding)
: base(columns, stream, encoding) { }
#region Overrides of HtmlTableGenerator
/// <inheritdoc />
protected override void OnOpen()
{
Writer.Write($"<details class=\"{DetailsClass}\" open>");
Writer.Write(HtmlTools.Wrap("summary", Summary));
base.OnOpen();
}
/// <inheritdoc />
protected override void OnClose()
{
base.OnClose();
Writer.Write($"</details>");
}
#endregion
}
@@ -0,0 +1,116 @@
using ReportGeneration.Abstract;
using ReportGeneration.Interface;
using System.IO;
using System.Reflection;
using System.Text;
namespace ReportGeneration.Generators;
public class HtmlDocumentGenerator : DocumentGeneratorBase
{
private int _sectionLevel = 0;
public string Title { get; init; } = string.Empty;
/// <inheritdoc />
public HtmlDocumentGenerator() { }
/// <inheritdoc />
public HtmlDocumentGenerator(string filePath) : base(filePath)
{
Title = Path.GetFileNameWithoutExtension(filePath);
}
/// <inheritdoc />
public HtmlDocumentGenerator(Stream stream) : base(stream) { }
/// <inheritdoc />
public HtmlDocumentGenerator(Stream stream, Encoding encoding) : base(stream, encoding) { }
#region State
/// <inheritdoc />
protected override void OnOpen()
{
base.OnOpen();
// Init html document
Write("<!DOCTYPE html>");
Write("<html>");
// Header
Write("<head>");
Write(HtmlTools.Wrap("title", Title));
Write("<meta charset=\"utf-8\">");
Write("<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">");
Write("<style>");
using (var stream = Resources.Get("Style.css"))
{
using var streamReader = new StreamReader(stream);
Write(streamReader);
}
Write("</style>");
Write("</head>");
// Init body
Write("<body>");
}
/// <inheritdoc />
protected override void OnClose()
{
base.OnClose();
// End document
Write("</body>");
Write("</html>");
}
#endregion
#region Writing
/// <inheritdoc cref="AppendParagraph(string)" />
public IDocumentGenerator AppendParagraph(string? text, string? @class) =>
Append(HtmlTools.Wrap("p", text, @class));
/// <inheritdoc />
public override IDocumentGenerator AppendParagraph(string? text = default) =>
AppendParagraph(text, default);
/// <inheritdoc />
public override IDocumentGenerator AppendHeading(int level, string text) =>
Append($"<h{level} id=\"{text}\">{text}</h{level}>");
/// <inheritdoc />
protected override ITableGenerator MakeTable(int columns, Stream stream) =>
new CollapsibleHtmlTableGenerator(columns, stream);
/// <inheritdoc />
public override string FormatImage(string path, IBounds? bounds = default) =>
FormatImage(path, default, bounds);
/// <inheritdoc cref="FormatImage(string,IBounds)" />
public string FormatImage(string path, string? @class, IBounds? bounds = default) =>
HtmlTools.FormatImage(path, @class, bounds);
#endregion
#region Resource Management
private static class Resources
{
private static readonly Assembly assembly = Assembly.GetExecutingAssembly();
private static readonly string basePath =
typeof(HtmlDocumentGenerator).Namespace + ".Resources.";
public static Stream Get(string fileName) =>
assembly.GetManifestResourceStream(basePath + fileName) ??
throw new FileNotFoundException("Could not get resource", fileName);
}
#endregion
}
@@ -0,0 +1,77 @@
using ReportGeneration.Abstract;
using ReportGeneration.Interface;
using System.Collections.Generic;
using System.IO;
using System.Text;
namespace ReportGeneration.Generators;
internal class HtmlTableGenerator : TableGeneratorBase
{
public string Class { get; init; }
private static readonly (string start, string end) rowFormat = ("<tr>", "</tr>");
private static readonly (string start, string end) headerFormat = ("<th>", "</th>");
private static readonly (string start, string end) columnFormat = ("<td>", "</td>");
/// <inheritdoc />
public HtmlTableGenerator(int columns) : base(columns) { }
/// <inheritdoc />
public HtmlTableGenerator(int columns, Stream stream) : base(columns, stream) { }
/// <inheritdoc />
public HtmlTableGenerator(int columns, Stream stream, Encoding encoding) : base(columns, stream,
encoding)
{ }
#region State
/// <inheritdoc />
protected override void OnOpen()
{
base.OnOpen();
Writer.Write($"<table class={Class}>");
}
/// <inheritdoc />
protected override void OnClose()
{
base.OnClose();
Writer.Write("</table>");
}
#endregion
#region Writing
/// <inheritdoc />
public override ITableGenerator AppendHeader(IEnumerable<string> row) =>
AppendRow(row, rowFormat, headerFormat);
/// <inheritdoc />
public override ITableGenerator AppendRow(IEnumerable<string> row) =>
AppendRow(row, rowFormat, columnFormat);
private ITableGenerator AppendRow(
IEnumerable<string> row,
(string, string) rowFormat,
(string, string) columnFormat
)
{
var (rowStart, rowEnd) = rowFormat;
var (colStart, colEnd) = columnFormat;
this
.Write(rowStart)
.Write(colStart)
.Write(string.Join(colEnd + colStart, row))
.Write(colEnd)
.Write(rowEnd);
return this;
}
#endregion
}
@@ -0,0 +1,59 @@
using ReportGeneration.Interface;
namespace ReportGeneration.Generators;
internal static class HtmlTools
{
public static string Wrap(string tag, string? content, string? @class = default) =>
$"<{tag} class={@class}>{content}</{tag}>";
public static string FormatImage(string path, string? @class = default, IBounds? bounds = default)
{
var style = bounds is null
? string.Empty
: GetCssStyle(bounds);
path += path.EndsWith(".png") ? string.Empty : ".png";
return $"<img src=\"{path}\" style=\"{style}\" class={@class} />";
}
private static string GetCssStyle(IBounds bounds)
{
var style = string.Empty;
// Width
if (bounds.Width.HasValue)
{
style += $"width:{bounds.Width}{bounds.Unit};";
}
if (bounds.MinWidth.HasValue)
{
style += $"min-width:{bounds.MinWidth}{bounds.Unit};";
}
if (bounds.MaxWidth.HasValue)
{
style += $"max-width:{bounds.MaxWidth}{bounds.Unit};";
}
// Height
if (bounds.Height.HasValue)
{
style += $"height:{bounds.Height}{bounds.Unit};";
}
if (bounds.MinHeight.HasValue)
{
style += $"min-height:{bounds.MinHeight}{bounds.Unit};";
}
if (bounds.MaxHeight.HasValue)
{
style += $"max-height:{bounds.MaxHeight}{bounds.Unit};";
}
return style;
}
}
@@ -0,0 +1,45 @@
using ReportGeneration.Abstract;
using ReportGeneration.Interface;
using System.IO;
using System.Text;
namespace ReportGeneration.Generators;
public class MarkdownDocumentGenerator : DocumentGeneratorBase
{
/// <inheritdoc />
public MarkdownDocumentGenerator() { }
/// <inheritdoc />
public MarkdownDocumentGenerator(string filePath) : base(filePath) { }
/// <inheritdoc />
public MarkdownDocumentGenerator(Stream stream) : base(stream) { }
/// <inheritdoc />
public MarkdownDocumentGenerator(Stream stream, Encoding encoding) : base(stream, encoding) { }
#region Writing
/// <inheritdoc />
public override IDocumentGenerator AppendHeading(int level, string text) =>
AppendParagraph(new string('#', level) + ' ' + text);
/// <inheritdoc />
protected override ITableGenerator MakeTable(int columns, Stream stream) =>
new MarkdownTableGenerator(columns, stream);
/// <inheritdoc />
public override IDocumentGenerator AppendParagraph(string? text = default)
{
AppendLine(text);
AppendLine();
return this;
}
#endregion
/// <inheritdoc />
public override string FormatImage(string path, IBounds? bounds = default) =>
HtmlTools.FormatImage(path, default, bounds);
}
@@ -0,0 +1,37 @@
using ReportGeneration.Abstract;
using ReportGeneration.Interface;
using System.Collections.Generic;
using System.IO;
using System.Text;
namespace ReportGeneration.Generators;
internal class MarkdownTableGenerator : TableGeneratorBase
{
private const string ColumnSeparator = " | ";
/// <inheritdoc />
public MarkdownTableGenerator(int columns)
: base(columns) { }
/// <inheritdoc />
public MarkdownTableGenerator(int columns, Stream stream)
: base(columns, stream) { }
/// <inheritdoc />
public MarkdownTableGenerator(int columns, Stream stream, Encoding encoding)
: base(columns, stream, encoding) { }
/// <inheritdoc />
public override ITableGenerator AppendHeader(IEnumerable<string> row) =>
this
.AppendRow(row)
.AppendRow("---");
/// <inheritdoc />
public override ITableGenerator AppendRow(IEnumerable<string> row)
{
Writer.WriteLine(ColumnSeparator + string.Join(" | ", row) + ColumnSeparator);
return this;
}
}
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
<IncludeSymbols>True</IncludeSymbols>
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\ReportGeneration.Abstract\ReportGeneration.Abstract.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,17 @@
td, th {
border: 1px solid #777;
padding: .5rem;
text-align: center
}
table {
border-collapse: collapse
}
tbody tr:nth-child(odd) {
background: #eee
}
caption {
font-size: .8rem
}
@@ -0,0 +1,12 @@
namespace ReportGeneration.Interface;
public interface IBounds
{
public string Unit { get; }
public int? MinWidth { get; }
public int? MinHeight { get; }
public int? MaxWidth { get; }
public int? MaxHeight { get; }
public int? Width { get; }
public int? Height { get; }
}
@@ -0,0 +1,18 @@
using System;
namespace ReportGeneration.Interface;
public interface IDocumentGenerator : IStreamWriter
{
IDocumentGenerator Append(string? text = default);
IDocumentGenerator AppendLine(string? text = default);
IDocumentGenerator AppendParagraph(string? text = default);
IDocumentGenerator AppendHeading(int level, string text);
IDocumentGenerator AppendTable(int columns, Action<ITableGenerator> table);
string FormatImage(string path, IBounds? bounds = default);
}
@@ -0,0 +1,40 @@
using System;
using System.IO;
namespace ReportGeneration.Interface;
public interface IStreamWriter : IDisposable
{
/// <inheritdoc cref="TextWriter.WriteLine(string)"/>
IStreamWriter Write(string? text = default);
/// <inheritdoc cref="TextWriter.WriteLine(string)"/>
IStreamWriter WriteLine(string? text = default);
/// <summary>
/// <para>Writes the contents of the given <paramref name="writer"/>
/// to the internal <see cref="Stream"/></para>
/// </summary>
IStreamWriter Write(IStreamWriter writer);
/// <summary>
/// <para>Writes the contents of the given <paramref name="reader"/>
/// to the internal <see cref="Stream"/></para>
/// </summary>
IStreamWriter Write(StreamReader reader);
/// <summary>
/// Finalizes the content writer
/// </summary>
void Close();
/// <summary>
/// Initializes the content writer
/// </summary>
void Open();
/// <summary>
/// Reads the content of the underlying stream
/// </summary>
StreamReader Read();
}
@@ -0,0 +1,16 @@
using System.Collections.Generic;
namespace ReportGeneration.Interface;
public interface ITableGenerator : IStreamWriter
{
int Columns { get; }
ITableGenerator AppendHeader(string content);
ITableGenerator AppendHeader(IEnumerable<string> row);
ITableGenerator AppendHeader(IEnumerable<IEnumerable<string>> rows);
ITableGenerator AppendRow(string content);
ITableGenerator AppendRow(IEnumerable<string> row);
ITableGenerator AppendRows(IEnumerable<IEnumerable<string>> rows);
}
@@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
<IncludeSymbols>True</IncludeSymbols>
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
</PropertyGroup>
</Project>
+34
View File
@@ -0,0 +1,34 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReportGeneration.Abstract", "ReportGeneration.Abstract\ReportGeneration.Abstract.csproj", "{CE8FC7DA-E4DB-4A28-99CC-82D9EE72A290}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReportGeneration.Generators", "ReportGeneration.Generators\ReportGeneration.Generators.csproj", "{82729833-58A0-4694-AD41-EE41A659B413}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReportGeneration.Interface", "ReportGeneration.Interface\ReportGeneration.Interface.csproj", "{2C6CA0F6-0656-4C0A-8B3D-039C37EE5021}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{CE8FC7DA-E4DB-4A28-99CC-82D9EE72A290}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CE8FC7DA-E4DB-4A28-99CC-82D9EE72A290}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CE8FC7DA-E4DB-4A28-99CC-82D9EE72A290}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CE8FC7DA-E4DB-4A28-99CC-82D9EE72A290}.Release|Any CPU.Build.0 = Release|Any CPU
{82729833-58A0-4694-AD41-EE41A659B413}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{82729833-58A0-4694-AD41-EE41A659B413}.Debug|Any CPU.Build.0 = Debug|Any CPU
{82729833-58A0-4694-AD41-EE41A659B413}.Release|Any CPU.ActiveCfg = Release|Any CPU
{82729833-58A0-4694-AD41-EE41A659B413}.Release|Any CPU.Build.0 = Release|Any CPU
{2C6CA0F6-0656-4C0A-8B3D-039C37EE5021}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2C6CA0F6-0656-4C0A-8B3D-039C37EE5021}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2C6CA0F6-0656-4C0A-8B3D-039C37EE5021}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2C6CA0F6-0656-4C0A-8B3D-039C37EE5021}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal