Skip to content

Commit

Permalink
Add Kaligraphy package source;
Browse files Browse the repository at this point in the history
  • Loading branch information
onepiecefreak3 committed Jan 7, 2025
1 parent 000ffaf commit c2eef18
Show file tree
Hide file tree
Showing 16 changed files with 573 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using Kaligraphy.Contract.DataClasses.Generation.Packing;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;

namespace Kaligraphy.Contract.DataClasses.Generation
{
public class FontImageData
{
public required Image<Rgba32> Image { get; init; }

public required IList<PackedGylphData> Glyphs { get; init; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using SixLabors.ImageSharp;

namespace Kaligraphy.Contract.DataClasses.Generation.Packing
{
public class PackedElement<TElement>
{
public required TElement Element { get; init; }
public required Point Position { get; init; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Kaligraphy.Contract.DataClasses.Generation.Packing
{
public class PackedGylphData : PackedElement<GlyphData>
{
}
}
18 changes: 18 additions & 0 deletions src/lib/Kaligraphy.Contract/DataClasses/GlyphData.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp;

namespace Kaligraphy.Contract.DataClasses
{
public class GlyphData
{
/// <summary>
/// The glyph.
/// </summary>
public required Image<Rgba32> Glyph { get; init; }

/// <summary>
/// Gets a description of the glyph, including position and size of the glyph after additional adjustments.
/// </summary>
public required GlyphDescriptionData Description { get; init; }
}
}
17 changes: 17 additions & 0 deletions src/lib/Kaligraphy.Contract/DataClasses/GlyphDescriptionData.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using SixLabors.ImageSharp;

namespace Kaligraphy.Contract.DataClasses
{
public class GlyphDescriptionData
{
/// <summary>
/// The position into the glyph, where the non-white space starts.
/// </summary>
public required Point Position { get; init; }

/// <summary>
/// The size of the non-white space glyph.
/// </summary>
public required Size Size { get; init; }
}
}
10 changes: 10 additions & 0 deletions src/lib/Kaligraphy.Contract/Generation/Packing/IBinPacker.T2.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using Kaligraphy.Contract.DataClasses.Generation.Packing;

namespace Kaligraphy.Contract.Generation.Packing
{
public interface IBinPacker<in TElement, out TPacked>
where TPacked : PackedElement<TElement>
{
IEnumerable<TPacked> Pack(IEnumerable<TElement> elements);
}
}
13 changes: 13 additions & 0 deletions src/lib/Kaligraphy.Contract/Kaligraphy.Contract.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">

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

<ItemGroup>
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.6" />
</ItemGroup>

</Project>
28 changes: 28 additions & 0 deletions src/lib/Kaligraphy.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.12.35514.174 d17.12
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kaligraphy", "Kaligraphy\Kaligraphy.csproj", "{74A8B153-8D2A-49C9-9EB6-48629B68B6A6}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kaligraphy.Contract", "Kaligraphy.Contract\Kaligraphy.Contract.csproj", "{B4099C9C-4132-41B3-A908-68395AD1807A}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{74A8B153-8D2A-49C9-9EB6-48629B68B6A6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{74A8B153-8D2A-49C9-9EB6-48629B68B6A6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{74A8B153-8D2A-49C9-9EB6-48629B68B6A6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{74A8B153-8D2A-49C9-9EB6-48629B68B6A6}.Release|Any CPU.Build.0 = Release|Any CPU
{B4099C9C-4132-41B3-A908-68395AD1807A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B4099C9C-4132-41B3-A908-68395AD1807A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B4099C9C-4132-41B3-A908-68395AD1807A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B4099C9C-4132-41B3-A908-68395AD1807A}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal
35 changes: 35 additions & 0 deletions src/lib/Kaligraphy/DataClasses/Generation/Packing/BinPackerNode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using SixLabors.ImageSharp;

namespace Kaligraphy.DataClasses.Generation.Packing
{
/// <summary>
/// A node representing part of the canvas in packing.
/// </summary>
internal class BinPackerNode
{
/// <summary>
/// The position this node is set on the canvas.
/// </summary>
public required Point Position { get; init; }

/// <summary>
/// The size this node represents on the canvas.
/// </summary>
public required Size Size { get; init; }

/// <summary>
/// Is this node is already occupied by a box.
/// </summary>
public bool IsOccupied { get; set; }

/// <summary>
/// The right headed node.
/// </summary>
public BinPackerNode? RightNode { get; set; }

/// <summary>
/// The bottom headed node.
/// </summary>
public BinPackerNode? BottomNode { get; set; }
}
}
89 changes: 89 additions & 0 deletions src/lib/Kaligraphy/Generation/FontTextureGenerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
using Kaligraphy.Contract.DataClasses;
using Kaligraphy.Contract.DataClasses.Generation;
using Kaligraphy.Contract.DataClasses.Generation.Packing;
using Kaligraphy.Generation.Packing;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Drawing;
using SixLabors.ImageSharp.Drawing.Processing;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;

namespace Kaligraphy.Generation
{
/// <summary>
/// Generates textures out of a given list of glyphs.
/// </summary>
public class FontTextureGenerator
{
private readonly Size _canvasSize;
private readonly FontBinPacker _fontPacker;

/// <summary>
/// Creates a new instance of <see cref="FontTextureGenerator"/>.
/// </summary>
/// <param name="canvasSize">The size of the canvas to draw on.</param>
/// <param name="margin">The margin to the top and left side of each texture.</param>
public FontTextureGenerator(Size canvasSize, int margin)
{
_canvasSize = canvasSize;
_fontPacker = new FontBinPacker(canvasSize, margin);
}

/// <summary>
/// Generate font textures for the given glyphs.
/// </summary>
/// <param name="glyphs">The enumeration of adjusted glyphs.</param>
/// <param name="textureCount">The maximum texture count.</param>
/// <returns>The generated textures.</returns>
public IList<FontImageData> Generate(IList<GlyphData> glyphs, int textureCount = -1)
{
var fontTextures = new List<FontImageData>(textureCount >= 0 ? textureCount : 0);

IList<GlyphData> remainingGlyphs = glyphs;
while (remainingGlyphs.Count > 0)
{
// Stop if the texture limit is reached
if (textureCount > 0 && fontTextures.Count >= textureCount)
break;

// Create new font texture to draw on.
var fontCanvas = new Image<Rgba32>(_canvasSize.Width, _canvasSize.Height);

// Draw each positioned glyph on the font texture
var packedGlyphs = new List<PackedGylphData>(remainingGlyphs.Count);
foreach (PackedGylphData packedGlyph in _fontPacker.Pack(remainingGlyphs))
{
DrawGlyph(fontCanvas, packedGlyph);
packedGlyphs.Add(packedGlyph);
}

var fontImage = new FontImageData
{
Image = fontCanvas,
Glyphs = packedGlyphs
};
fontTextures.Add(fontImage);

// Remove every handled glyph
remainingGlyphs = remainingGlyphs.Except(packedGlyphs.Select(g => g.Element)).ToList();
}

return fontTextures;
}

/// <summary>
/// Draws a glyph onto the font texture.
/// </summary>
/// <param name="fontImage">The font texture to draw on.</param>
/// <param name="packedGlyph">The adjusted glyph positioned in relation to the texture.</param>
private void DrawGlyph(Image<Rgba32> fontImage, PackedGylphData packedGlyph)
{
GlyphData glyph = packedGlyph.Element;

var destRect = new Rectangle(packedGlyph.Position, glyph.Description.Size);
var sourceRect = new Rectangle(glyph.Description.Position, glyph.Description.Size);

fontImage.Mutate(i => i.Clip(new RectangularPolygon(destRect), context => context.DrawImage(glyph.Glyph, sourceRect, 1f)));
}
}
}
133 changes: 133 additions & 0 deletions src/lib/Kaligraphy/Generation/Packing/BinPacker.T2.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
using Kaligraphy.Contract.DataClasses.Generation.Packing;
using Kaligraphy.Contract.Generation.Packing;
using Kaligraphy.DataClasses.Generation.Packing;
using SixLabors.ImageSharp;

namespace Kaligraphy.Generation.Packing
{
public abstract class BinPacker<TElement, TPacked> : IBinPacker<TElement, TPacked>
where TPacked : PackedElement<TElement>
{
/// <summary>
/// Gets the total size of the canvas.
/// </summary>
protected Size CanvasSize { get; }

/// <summary>
/// The margin between all elements.
/// </summary>
protected Size Margin { get; }

/// <summary>
/// Creates a new instance of <see cref="BinPacker{TElement,TPacked}"/>"/>.
/// </summary>
/// <param name="canvasSize">The total size of the canvas.</param>
/// <param name="margin">The margin between all elements.</param>
protected BinPacker(Size canvasSize, Size margin)
{
CanvasSize = canvasSize;
Margin = new Size(margin);
}

/// <summary>
/// Pack an enumeration of white space adjusted glyphs into the given canvas.
/// </summary>
/// <param name="elements">The enumeration of glyphs.</param>
/// <returns>Position information to a glyph.</returns>
public IEnumerable<TPacked> Pack(IEnumerable<TElement> elements)
{
var rootNode = new BinPackerNode
{
Position = Point.Empty,
Size = CanvasSize - Margin
};

foreach (TElement element in elements.OrderByDescending(CalculateVolume))
{
Size elementSize = CalculateSize(element);

BinPackerNode? foundNode = FindNode(rootNode, elementSize);
if (foundNode == null)
continue;

SplitNode(foundNode, elementSize);
yield return CreatePackedElement(element, foundNode.Position);
}
}

/// <summary>
/// Calculates the volume of an element.
/// </summary>
/// <param name="element">The element to calculate the volume from.</param>
/// <returns>The calculated volume.</returns>
protected abstract int CalculateVolume(TElement element);

/// <summary>
/// Calculates the size of the element.
/// </summary>
/// <param name="element">The element to calculate the size from.</param>
/// <returns>The calculated size.</returns>
protected abstract Size CalculateSize(TElement element);

/// <summary>
/// Creates the packed element.
/// </summary>
/// <param name="element">The element to pack.</param>
/// <param name="position">The position of the element.</param>
/// <returns>The packed element.</returns>
protected abstract TPacked CreatePackedElement(TElement element, Point position);

/// <summary>
/// Find a node to fit the box in.
/// </summary>
/// <param name="node">The current node to search through.</param>
/// <param name="boxSize">The size of the box.</param>
/// <returns>The found node.</returns>
private BinPackerNode? FindNode(BinPackerNode node, Size boxSize)
{
if (node.IsOccupied)
{
BinPackerNode? nextNode = null;
if (node.BottomNode is not null)
{
nextNode = FindNode(node.BottomNode, boxSize);
if (nextNode is null && node.RightNode is not null)
nextNode = FindNode(node.RightNode, boxSize);
}
else
{
if (node.RightNode is not null)
nextNode = FindNode(node.RightNode, boxSize);
}

return nextNode;
}

if (boxSize.Width <= node.Size.Width && boxSize.Height <= node.Size.Height)
return node;

return null;
}

/// <summary>
/// Splits a node to fit the box.
/// </summary>
/// <param name="node">The node to split.</param>
/// <param name="boxSize">The size of the box.</param>
private void SplitNode(BinPackerNode node, Size boxSize)
{
node.IsOccupied = true;

node.BottomNode = new BinPackerNode
{
Position = new Point(node.Position.X + boxSize.Width, node.Position.Y),
Size = new Size(node.Size.Width - boxSize.Width, node.Size.Height),
};
node.RightNode = new BinPackerNode
{
Position = new Point(node.Position.X, node.Position.Y + boxSize.Height),
Size = new Size(boxSize.Width, node.Size.Height - boxSize.Height)
};
}
}
}
Loading

0 comments on commit c2eef18

Please sign in to comment.