Skip to content
Open
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
22da9f1
Basic first pass functionality (pre-cleanup)
JimBobSquarePants Sep 22, 2025
a8e63f1
Allow layered path generation by default
JimBobSquarePants Oct 3, 2025
5c41a5f
Decorate after rendering the glyph.
JimBobSquarePants Oct 3, 2025
68c3b0e
Can now normalize decorations
JimBobSquarePants Oct 3, 2025
d8deb96
Make browser-like the default. It's MUCH nicer
JimBobSquarePants Oct 3, 2025
8efed80
Move renderers. Reduce allocations
JimBobSquarePants Oct 3, 2025
089a278
Remove some allocations
JimBobSquarePants Oct 3, 2025
19c3f50
Can now load ColrV1 table.
JimBobSquarePants Oct 12, 2025
af70b11
Normalize paint commands
JimBobSquarePants Oct 17, 2025
85b0633
Add ColrV1GlyphSource
JimBobSquarePants Oct 17, 2025
77b46e2
Can now render COLRv1
JimBobSquarePants Oct 21, 2025
e31c8a0
Normalize COLR glyph source and pass clipping to renderer
JimBobSquarePants Oct 21, 2025
a6798bb
Clean-up metrics cache and public API
JimBobSquarePants Oct 22, 2025
94e4d00
Fix COLRv1 rendering
JimBobSquarePants Oct 27, 2025
a0f90b8
Make units internal
JimBobSquarePants Oct 27, 2025
819a1f8
Handle known colors in SVG
JimBobSquarePants Oct 27, 2025
7a7af50
Fix SolidPaint COLRv1 transforms
JimBobSquarePants Oct 27, 2025
c816bad
Ensure decorations are positioned correctly when sharing
JimBobSquarePants Oct 27, 2025
bb4bacd
Cleanup, use ClipQuad for clipping bounds to handle transforms
JimBobSquarePants Oct 28, 2025
9873490
Update tests
JimBobSquarePants Oct 28, 2025
f3e5ce5
Add some metrics, disable drawing comparison
JimBobSquarePants Oct 28, 2025
de5872e
Merge branch 'main' into js/issue-462
JimBobSquarePants Oct 30, 2025
d6b9eb3
Use concurrent dictionary, fix namespace
JimBobSquarePants Oct 30, 2025
2b735f3
Use GetOrAdd
JimBobSquarePants Oct 30, 2025
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
11 changes: 2 additions & 9 deletions SixLabors.Fonts.sln
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.2.32519.379
# Visual Studio Version 18
VisualStudioVersion = 18.0.11012.119
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_root", "_root", "{C317F1B1-D75E-4C6D-83EB-80367343E0D7}"
ProjectSection(SolutionItems) = preProject
Expand Down Expand Up @@ -73,8 +73,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "UnicodeTestData", "UnicodeT
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SixLabors.Fonts.Benchmarks", "tests\SixLabors.Fonts.Benchmarks\SixLabors.Fonts.Benchmarks\SixLabors.Fonts.Benchmarks.csproj", "{FB8FDC5F-7FEB-4132-9133-C25E05C0B3D9}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DrawWithImageSharp", "samples\DrawWithImageSharp\DrawWithImageSharp.csproj", "{3D3F6164-6DE9-433F-8B20-61A40F53F343}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -101,10 +99,6 @@ Global
{FB8FDC5F-7FEB-4132-9133-C25E05C0B3D9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FB8FDC5F-7FEB-4132-9133-C25E05C0B3D9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FB8FDC5F-7FEB-4132-9133-C25E05C0B3D9}.Release|Any CPU.Build.0 = Release|Any CPU
{3D3F6164-6DE9-433F-8B20-61A40F53F343}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3D3F6164-6DE9-433F-8B20-61A40F53F343}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3D3F6164-6DE9-433F-8B20-61A40F53F343}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3D3F6164-6DE9-433F-8B20-61A40F53F343}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -119,7 +113,6 @@ Global
{ABB6E111-672F-4846-88D6-C49C6CD01606} = {249327CF-1415-428B-8EEA-8C7705B1DE8F}
{654DD381-B93D-4459-B669-296F5D9172ED} = {56801022-D71A-4FBE-BC5B-CBA08E2284EC}
{FB8FDC5F-7FEB-4132-9133-C25E05C0B3D9} = {56801022-D71A-4FBE-BC5B-CBA08E2284EC}
{3D3F6164-6DE9-433F-8B20-61A40F53F343} = {71A3911C-D6B9-4EBE-9691-2FE28BDF462E}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {38F4B47F-4F74-40F5-8707-C0EF1D0BDF92}
Expand Down
1 change: 1 addition & 0 deletions samples/DrawWithImageSharp/BoundingBoxes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System.Numerics;
using SixLabors.Fonts;
using SixLabors.Fonts.Rendering;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Drawing;
using SixLabors.ImageSharp.Drawing.Processing;
Expand Down
3 changes: 2 additions & 1 deletion samples/DrawWithImageSharp/CustomGlyphBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System.Numerics;
using SixLabors.Fonts;
using SixLabors.Fonts.Rendering;
using SixLabors.ImageSharp.Drawing;
using SixLabors.ImageSharp.Drawing.Text;

Expand All @@ -13,7 +14,7 @@ namespace DrawWithImageSharp;
/// </summary>
internal class CustomGlyphBuilder : GlyphBuilder
{
private readonly List<FontRectangle> glyphBounds = new();
private readonly List<FontRectangle> glyphBounds = [];

public CustomGlyphBuilder()
{
Expand Down
2 changes: 1 addition & 1 deletion samples/DrawWithImageSharp/DrawWithImageSharp.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
</Content>
</ItemGroup>

<ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\SixLabors.Fonts\SixLabors.Fonts.csproj" />
</ItemGroup>

Expand Down
2 changes: 1 addition & 1 deletion samples/DrawWithImageSharp/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,7 @@ public static void RenderTextProcessorWithAlignment(
new SolidBrush(Color.Yellow),
null)));

img[size.Width / 2, size.Height / 2] = Color.White;
img[size.Width / 2, size.Height / 2] = Color.White.ToPixel<Rgba32>();

string h = ha.ToString().Replace(nameof(HorizontalAlignment), string.Empty).ToLower();
string v = va.ToString().Replace(nameof(VerticalAlignment), string.Empty).ToLower();
Expand Down
10 changes: 5 additions & 5 deletions samples/DrawWithImageSharp/TextAlignmentSample.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System.Numerics;
using SixLabors.Fonts;
using SixLabors.Fonts.Rendering;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Drawing;
using SixLabors.ImageSharp.Drawing.Processing;
Expand All @@ -15,7 +16,7 @@ public static class TextAlignmentSample
{
public static void Generate(Font font)
{
using var img = new Image<Rgba32>(1000, 1000);
using Image<Rgba32> img = new(1000, 1000);
img.Mutate(x => x.Fill(Color.White));

foreach (VerticalAlignment v in Enum.GetValues(typeof(VerticalAlignment)))
Expand Down Expand Up @@ -63,9 +64,9 @@ public static void Draw(Image<Rgba32> img, Font font, VerticalAlignment vert, Ho
break;
}

var glyphBuilder = new CustomGlyphBuilder();
CustomGlyphBuilder glyphBuilder = new();

var renderer = new TextRenderer(glyphBuilder);
TextRenderer renderer = new(glyphBuilder);

TextOptions textOptions = new(font)
{
Expand All @@ -82,8 +83,7 @@ public static void Draw(Image<Rgba32> img, Font font, VerticalAlignment vert, Ho
IEnumerable<IPath> shapesToDraw = glyphBuilder.Paths;
img.Mutate(x => x.Fill(Color.Black, glyphBuilder.Paths));

Rgba32 f = Color.Fuchsia;
f.A = 128;
Color f = Color.Fuchsia.WithAlpha(.5F);
img.Mutate(x => x.Fill(Color.Black, glyphBuilder.Paths));
img.Mutate(x => x.Draw(f, 1, glyphBuilder.Boxes));
img.Mutate(x => x.Draw(Color.Lime, 1, glyphBuilder.TextBox));
Expand Down
10 changes: 5 additions & 5 deletions samples/DrawWithImageSharp/TextAlignmentWrapped.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System.Numerics;
using SixLabors.Fonts;
using SixLabors.Fonts.Rendering;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Drawing;
using SixLabors.ImageSharp.Drawing.Processing;
Expand All @@ -18,7 +19,7 @@ public static void Generate(Font font)
const int wrappingWidth = 400;
const int size = (wrappingWidth + (wrappingWidth / 3)) * 3;

using var img = new Image<Rgba32>(size, size);
using Image<Rgba32> img = new(size, size);
img.Mutate(x => x.Fill(Color.White));

foreach (VerticalAlignment v in Enum.GetValues(typeof(VerticalAlignment)))
Expand Down Expand Up @@ -66,9 +67,9 @@ public static void Draw(Image<Rgba32> img, Font font, VerticalAlignment vert, Ho
break;
}

var glyphBuilder = new CustomGlyphBuilder();
CustomGlyphBuilder glyphBuilder = new();

var renderer = new TextRenderer(glyphBuilder);
TextRenderer renderer = new(glyphBuilder);

TextOptions textOptions = new(font)
{
Expand All @@ -85,8 +86,7 @@ public static void Draw(Image<Rgba32> img, Font font, VerticalAlignment vert, Ho
IEnumerable<IPath> shapesToDraw = glyphBuilder.Paths;
img.Mutate(x => x.Fill(Color.Black, glyphBuilder.Paths));

Rgba32 f = Color.Fuchsia;
f.A = 128;
Color f = Color.Fuchsia.WithAlpha(.5F);
img.Mutate(x => x.Fill(Color.Black, glyphBuilder.Paths));
img.Mutate(x => x.Draw(f, 1, glyphBuilder.Boxes));
img.Mutate(x => x.Draw(Color.Lime, 1, glyphBuilder.TextBox));
Expand Down
45 changes: 30 additions & 15 deletions src/SixLabors.Fonts/BigEndianBinaryReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
namespace SixLabors.Fonts;

/// <summary>
/// BinaryReader using big-endian encoding.
/// A binary reader that reads in big-endian format.
/// </summary>
[DebuggerDisplay("Start: {StartOfStream}, Position: {BaseStream.Position}")]
internal sealed class BigEndianBinaryReader : IDisposable
Expand All @@ -19,6 +19,7 @@ internal sealed class BigEndianBinaryReader : IDisposable
/// </summary>
private readonly byte[] buffer = new byte[16];

private readonly long startOfStream;
private readonly bool leaveOpen;

/// <summary>
Expand All @@ -31,12 +32,10 @@ internal sealed class BigEndianBinaryReader : IDisposable
public BigEndianBinaryReader(Stream stream, bool leaveOpen)
{
this.BaseStream = stream;
this.StartOfStream = stream.Position;
this.startOfStream = stream.Position;
this.leaveOpen = leaveOpen;
}

private long StartOfStream { get; }

/// <summary>
/// Gets the underlying stream of the EndianBinaryReader.
/// </summary>
Expand All @@ -52,7 +51,7 @@ public void Seek(long offset, SeekOrigin origin)
// If SeekOrigin.Begin, the offset will be set to the start of stream position.
if (origin == SeekOrigin.Begin)
{
offset += this.StartOfStream;
offset += this.startOfStream;
}

this.BaseStream.Seek(offset, origin);
Expand All @@ -68,6 +67,13 @@ public byte ReadByte()
return this.buffer[0];
}

public TEnum ReadByte<TEnum>()
where TEnum : struct, Enum
{
TryConvert(this.ReadByte(), out TEnum value);
return value;
}

/// <summary>
/// Reads a single signed byte from the stream.
/// </summary>
Expand All @@ -78,9 +84,9 @@ public sbyte ReadSByte()
return unchecked((sbyte)this.buffer[0]);
}

public float ReadF2dot14()
public float ReadF2Dot14()
{
const float f2Dot14ToFloat = 16384.0f;
const float f2Dot14ToFloat = 16384F;
return this.ReadInt16() / f2Dot14ToFloat;
}

Expand All @@ -103,28 +109,35 @@ public TEnum ReadInt16<TEnum>()
return value;
}

/// <summary>
/// Reads a signed 16-bit integer in big-endian order, representing an FWORD value from the current stream position.
/// </summary>
/// <returns>A 16-bit signed integer read from the stream, interpreted as an FWORD value.</returns>
public short ReadFWORD() => this.ReadInt16();

public short[] ReadFWORDArray(int length) => this.ReadInt16Array(length);

/// <summary>
/// Reads an unsigned 16-bit integer (UFWORD) from the current stream and advances the position by two bytes.
/// </summary>
/// <returns>An unsigned 16-bit integer read from the current stream.</returns>
public ushort ReadUFWORD() => this.ReadUInt16();

/// <summary>
/// Reads a fixed 32-bit value from the stream.
/// 4 bytes are read.
/// Reads a 32-bit fixed-point number from the underlying data source and returns it as a single-precision
/// floating-point value.
/// </summary>
/// <returns>The 32-bit value read.</returns>
/// <returns>A <see cref="float"/> representing the fixed-point value read from the data source.</returns>
public float ReadFixed()
{
this.ReadInternal(this.buffer, 4);
return BinaryPrimitives.ReadInt32BigEndian(this.buffer) / 65536F;
}

/// <summary>
/// Reads a 32-bit signed integer from the stream, using the bit converter
/// for this reader. 4 bytes are read.
/// Reads a 4-byte signed integer from the current stream.
/// </summary>
/// <returns>The 32-bit integer read</returns>
/// <returns>The 32-bit signed integer read from the stream.</returns>
public int ReadInt32()
{
this.ReadInternal(this.buffer, 4);
Expand Down Expand Up @@ -273,12 +286,14 @@ public byte ReadUInt8()
/// for this reader. 3 bytes are read.
/// </summary>
/// <returns>The 24-bit unsigned integer read.</returns>
public int ReadUInt24()
public uint ReadUInt24()
{
byte highByte = this.ReadByte();
return (highByte << 16) | this.ReadUInt16();
return (uint)((highByte << 16) | this.ReadUInt16());
}

public uint ReadOffset24() => this.ReadUInt24();

/// <summary>
/// Reads a 32-bit unsigned integer from the stream, using the bit converter
/// for this reader. 4 bytes are read.
Expand Down
84 changes: 84 additions & 0 deletions src/SixLabors.Fonts/ClipQuad.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.

using System.Numerics;

namespace SixLabors.Fonts;

/// <summary>
/// Represents a rectangular clipping region as a convex quadrilateral.
/// Allows for transformation by rotation, skew, or non-uniform scaling,
/// resulting in non-axis-aligned edges.
/// </summary>
public readonly struct ClipQuad
{
/// <summary>
/// Initializes a new instance of the <see cref="ClipQuad"/> struct.
/// </summary>
/// <param name="topLeft">The top-left corner of the quadrilateral.</param>
/// <param name="topRight">The top-right corner of the quadrilateral.</param>
/// <param name="bottomRight">The bottom-right corner of the quadrilateral.</param>
/// <param name="bottomLeft">The bottom-left corner of the quadrilateral.</param>
public ClipQuad(Vector2 topLeft, Vector2 topRight, Vector2 bottomRight, Vector2 bottomLeft)
{
this.TopLeft = topLeft;
this.TopRight = topRight;
this.BottomRight = bottomRight;
this.BottomLeft = bottomLeft;
}

/// <summary>
/// Gets the top-left corner of the quadrilateral.
/// </summary>
public Vector2 TopLeft { get; }

/// <summary>
/// Gets the top-right corner of the quadrilateral.
/// </summary>
public Vector2 TopRight { get; }

/// <summary>
/// Gets the bottom-right corner of the quadrilateral.
/// </summary>
public Vector2 BottomRight { get; }

/// <summary>
/// Gets the bottom-left corner of the quadrilateral.
/// </summary>
public Vector2 BottomLeft { get; }

/// <summary>
/// Creates a <see cref="ClipQuad"/> from an axis-aligned <see cref="Bounds"/> and an optional transform.
/// </summary>
/// <param name="bounds">The bounds representing the untransformed rectangular area.</param>
/// <param name="transform">An optional transform to apply. If omitted, no transform is applied.</param>
/// <returns>A <see cref="ClipQuad"/> representing the transformed rectangle.</returns>
internal static ClipQuad FromBounds(in Bounds bounds, in Matrix3x2 transform)
{
Vector2 tl = Vector2.Transform(bounds.Min, transform);
Vector2 tr = Vector2.Transform(new Vector2(bounds.Max.X, bounds.Min.Y), transform);
Vector2 br = Vector2.Transform(bounds.Max, transform);
Vector2 bl = Vector2.Transform(new Vector2(bounds.Min.X, bounds.Max.Y), transform);
return new ClipQuad(tl, tr, br, bl);
}

/// <summary>
/// Determines whether the quadrilateral is axis-aligned within a small tolerance.
/// </summary>
/// <param name="tolerance">The tolerance for comparing parallel edges, typically a small epsilon.</param>
/// <returns>
/// <see langword="true"/> if opposite edges are parallel and of equal length; otherwise, <see langword="false"/>.
/// </returns>
public bool IsAxisAligned(float tolerance = 1E-4F)
{
Vector2 top = this.TopRight - this.TopLeft;
Vector2 bottom = this.BottomRight - this.BottomLeft;
Vector2 left = this.BottomLeft - this.TopLeft;
Vector2 right = this.BottomRight - this.TopRight;

bool horizontalParallel = MathF.Abs(Vector2.Dot(Vector2.Normalize(top), Vector2.Normalize(bottom)) - 1F) < tolerance;
bool verticalParallel = MathF.Abs(Vector2.Dot(Vector2.Normalize(left), Vector2.Normalize(right)) - 1F) < tolerance;

return horizontalParallel && verticalParallel;
}
}
Loading
Loading