diff --git a/SixLabors.Fonts.sln b/SixLabors.Fonts.sln index 7186cdbfa..70cd30215 100644 --- a/SixLabors.Fonts.sln +++ b/SixLabors.Fonts.sln @@ -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 @@ -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 @@ -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 @@ -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} diff --git a/samples/DrawWithImageSharp/BoundingBoxes.cs b/samples/DrawWithImageSharp/BoundingBoxes.cs index 97791c9b3..49815fcd1 100644 --- a/samples/DrawWithImageSharp/BoundingBoxes.cs +++ b/samples/DrawWithImageSharp/BoundingBoxes.cs @@ -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; diff --git a/samples/DrawWithImageSharp/CustomGlyphBuilder.cs b/samples/DrawWithImageSharp/CustomGlyphBuilder.cs index 049e2ca9b..e89f0f46f 100644 --- a/samples/DrawWithImageSharp/CustomGlyphBuilder.cs +++ b/samples/DrawWithImageSharp/CustomGlyphBuilder.cs @@ -3,6 +3,7 @@ using System.Numerics; using SixLabors.Fonts; +using SixLabors.Fonts.Rendering; using SixLabors.ImageSharp.Drawing; using SixLabors.ImageSharp.Drawing.Text; @@ -13,7 +14,7 @@ namespace DrawWithImageSharp; /// internal class CustomGlyphBuilder : GlyphBuilder { - private readonly List glyphBounds = new(); + private readonly List glyphBounds = []; public CustomGlyphBuilder() { diff --git a/samples/DrawWithImageSharp/DrawWithImageSharp.csproj b/samples/DrawWithImageSharp/DrawWithImageSharp.csproj index 89666fa2c..65ab1b059 100644 --- a/samples/DrawWithImageSharp/DrawWithImageSharp.csproj +++ b/samples/DrawWithImageSharp/DrawWithImageSharp.csproj @@ -41,7 +41,7 @@ - + diff --git a/samples/DrawWithImageSharp/Program.cs b/samples/DrawWithImageSharp/Program.cs index 6421cd32b..6136836cc 100644 --- a/samples/DrawWithImageSharp/Program.cs +++ b/samples/DrawWithImageSharp/Program.cs @@ -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(); string h = ha.ToString().Replace(nameof(HorizontalAlignment), string.Empty).ToLower(); string v = va.ToString().Replace(nameof(VerticalAlignment), string.Empty).ToLower(); diff --git a/samples/DrawWithImageSharp/TextAlignmentSample.cs b/samples/DrawWithImageSharp/TextAlignmentSample.cs index ea9fd6280..1bec4e407 100644 --- a/samples/DrawWithImageSharp/TextAlignmentSample.cs +++ b/samples/DrawWithImageSharp/TextAlignmentSample.cs @@ -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; @@ -15,7 +16,7 @@ public static class TextAlignmentSample { public static void Generate(Font font) { - using var img = new Image(1000, 1000); + using Image img = new(1000, 1000); img.Mutate(x => x.Fill(Color.White)); foreach (VerticalAlignment v in Enum.GetValues(typeof(VerticalAlignment))) @@ -63,9 +64,9 @@ public static void Draw(Image 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) { @@ -82,8 +83,7 @@ public static void Draw(Image img, Font font, VerticalAlignment vert, Ho IEnumerable 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)); diff --git a/samples/DrawWithImageSharp/TextAlignmentWrapped.cs b/samples/DrawWithImageSharp/TextAlignmentWrapped.cs index 01494ab37..24b272bf3 100644 --- a/samples/DrawWithImageSharp/TextAlignmentWrapped.cs +++ b/samples/DrawWithImageSharp/TextAlignmentWrapped.cs @@ -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; @@ -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(size, size); + using Image img = new(size, size); img.Mutate(x => x.Fill(Color.White)); foreach (VerticalAlignment v in Enum.GetValues(typeof(VerticalAlignment))) @@ -66,9 +67,9 @@ public static void Draw(Image 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) { @@ -85,8 +86,7 @@ public static void Draw(Image img, Font font, VerticalAlignment vert, Ho IEnumerable 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)); diff --git a/src/SixLabors.Fonts/BigEndianBinaryReader.cs b/src/SixLabors.Fonts/BigEndianBinaryReader.cs index 5f9c6aab7..b880498ee 100644 --- a/src/SixLabors.Fonts/BigEndianBinaryReader.cs +++ b/src/SixLabors.Fonts/BigEndianBinaryReader.cs @@ -9,7 +9,7 @@ namespace SixLabors.Fonts; /// -/// BinaryReader using big-endian encoding. +/// A binary reader that reads in big-endian format. /// [DebuggerDisplay("Start: {StartOfStream}, Position: {BaseStream.Position}")] internal sealed class BigEndianBinaryReader : IDisposable @@ -19,6 +19,7 @@ internal sealed class BigEndianBinaryReader : IDisposable /// private readonly byte[] buffer = new byte[16]; + private readonly long startOfStream; private readonly bool leaveOpen; /// @@ -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; } - /// /// Gets the underlying stream of the EndianBinaryReader. /// @@ -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); @@ -68,6 +67,13 @@ public byte ReadByte() return this.buffer[0]; } + public TEnum ReadByte() + where TEnum : struct, Enum + { + TryConvert(this.ReadByte(), out TEnum value); + return value; + } + /// /// Reads a single signed byte from the stream. /// @@ -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; } @@ -103,17 +109,25 @@ public TEnum ReadInt16() return value; } + /// + /// Reads a signed 16-bit integer in big-endian order, representing an FWORD value from the current stream position. + /// + /// A 16-bit signed integer read from the stream, interpreted as an FWORD value. public short ReadFWORD() => this.ReadInt16(); public short[] ReadFWORDArray(int length) => this.ReadInt16Array(length); + /// + /// Reads an unsigned 16-bit integer (UFWORD) from the current stream and advances the position by two bytes. + /// + /// An unsigned 16-bit integer read from the current stream. public ushort ReadUFWORD() => this.ReadUInt16(); /// - /// 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. /// - /// The 32-bit value read. + /// A representing the fixed-point value read from the data source. public float ReadFixed() { this.ReadInternal(this.buffer, 4); @@ -121,10 +135,9 @@ public float ReadFixed() } /// - /// 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. /// - /// The 32-bit integer read + /// The 32-bit signed integer read from the stream. public int ReadInt32() { this.ReadInternal(this.buffer, 4); @@ -273,12 +286,14 @@ public byte ReadUInt8() /// for this reader. 3 bytes are read. /// /// The 24-bit unsigned integer read. - 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(); + /// /// Reads a 32-bit unsigned integer from the stream, using the bit converter /// for this reader. 4 bytes are read. diff --git a/src/SixLabors.Fonts/ClipQuad.cs b/src/SixLabors.Fonts/ClipQuad.cs new file mode 100644 index 000000000..29bf7575a --- /dev/null +++ b/src/SixLabors.Fonts/ClipQuad.cs @@ -0,0 +1,84 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.Fonts; + +/// +/// 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. +/// +public readonly struct ClipQuad +{ + /// + /// Initializes a new instance of the struct. + /// + /// The top-left corner of the quadrilateral. + /// The top-right corner of the quadrilateral. + /// The bottom-right corner of the quadrilateral. + /// The bottom-left corner of the quadrilateral. + public ClipQuad(Vector2 topLeft, Vector2 topRight, Vector2 bottomRight, Vector2 bottomLeft) + { + this.TopLeft = topLeft; + this.TopRight = topRight; + this.BottomRight = bottomRight; + this.BottomLeft = bottomLeft; + } + + /// + /// Gets the top-left corner of the quadrilateral. + /// + public Vector2 TopLeft { get; } + + /// + /// Gets the top-right corner of the quadrilateral. + /// + public Vector2 TopRight { get; } + + /// + /// Gets the bottom-right corner of the quadrilateral. + /// + public Vector2 BottomRight { get; } + + /// + /// Gets the bottom-left corner of the quadrilateral. + /// + public Vector2 BottomLeft { get; } + + /// + /// Creates a from an axis-aligned and an optional transform. + /// + /// The bounds representing the untransformed rectangular area. + /// An optional transform to apply. If omitted, no transform is applied. + /// A representing the transformed rectangle. + 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); + } + + /// + /// Determines whether the quadrilateral is axis-aligned within a small tolerance. + /// + /// The tolerance for comparing parallel edges, typically a small epsilon. + /// + /// if opposite edges are parallel and of equal length; otherwise, . + /// + 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; + } +} diff --git a/src/SixLabors.Fonts/ColorFontSupport.cs b/src/SixLabors.Fonts/ColorFontSupport.cs index 5e93c9e34..b5441e858 100644 --- a/src/SixLabors.Fonts/ColorFontSupport.cs +++ b/src/SixLabors.Fonts/ColorFontSupport.cs @@ -1,21 +1,36 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. namespace SixLabors.Fonts; /// -/// Options for enabling color font support during layout and rendering. +/// Specifies which color font formats are enabled for layout and rendering. /// -[Flags] // flags is because in the future we might want to add support for additional color font storage formats and might want to support multiple at once. +/// +/// This enumeration allows a renderer to select which OpenType color font +/// technologies to honor when processing glyph runs. Multiple formats may be +/// enabled simultaneously. +/// +[Flags] public enum ColorFontSupport { /// - /// Don't try rendering color glyphs at all + /// Disable color font rendering entirely. All glyphs will be drawn as monochrome outlines. /// None = 0, /// - /// Render using glyphs accessed via Microsoft's COLR/CPAL table extensions to OpenType + /// Enable rendering of COLR version 0 color glyphs (layered solid colors defined by COLR/CPAL tables). /// - MicrosoftColrFormat = 1 + ColrV0 = 1, + + /// + /// Enable rendering of COLR version 1 color glyphs (paint graph-based color glyphs with gradients and transforms). + /// + ColrV1 = 2, + + /// + /// Enable rendering of color glyphs stored as SVG documents in the OpenType SVG table. + /// + Svg = 4 } diff --git a/src/SixLabors.Fonts/DecorationPositioningMode.cs b/src/SixLabors.Fonts/DecorationPositioningMode.cs new file mode 100644 index 000000000..e1a1529bb --- /dev/null +++ b/src/SixLabors.Fonts/DecorationPositioningMode.cs @@ -0,0 +1,24 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts; + +/// +/// Defines how text decorations (underline, overline, strikethrough) are positioned relative to font metrics. +/// +public enum DecorationPositioningMode +{ + /// + /// Uses the primary (base) font's metrics for the entire run or line, + /// ensuring a consistent decoration position across mixed fonts and scripts. + /// Matches typical browser behavior. + /// + PrimaryFont = 0, + + /// + /// Uses each glyph's own font metrics to position its decoration. + /// Decoration positions may vary between glyphs and fallback fonts within the same line. + /// Matches typical Microsoft Word behavior. + /// + GlyphFont = 1, +} diff --git a/src/SixLabors.Fonts/FileFontMetrics.cs b/src/SixLabors.Fonts/FileFontMetrics.cs index 4a30c3572..e1eda515b 100644 --- a/src/SixLabors.Fonts/FileFontMetrics.cs +++ b/src/SixLabors.Fonts/FileFontMetrics.cs @@ -126,11 +126,11 @@ public override bool TryGetGlyphMetrics( TextDecorations textDecorations, LayoutMode layoutMode, ColorFontSupport support, - [NotNullWhen(true)] out IReadOnlyList? metrics) + [NotNullWhen(true)] out GlyphMetrics? metrics) => this.fontMetrics.Value.TryGetGlyphMetrics(codePoint, textAttributes, textDecorations, layoutMode, support, out metrics); /// - internal override IReadOnlyList GetGlyphMetrics( + internal override GlyphMetrics GetGlyphMetrics( CodePoint codePoint, ushort glyphId, TextAttributes textAttributes, diff --git a/src/SixLabors.Fonts/Font.cs b/src/SixLabors.Fonts/Font.cs index 706f9c440..570493043 100644 --- a/src/SixLabors.Fonts/Font.cs +++ b/src/SixLabors.Fonts/Font.cs @@ -142,43 +142,43 @@ public bool TryGetPath([NotNullWhen(true)] out string? path) } /// - /// Gets the glyphs for the given codepoint. + /// Gets the glyph for the given codepoint. /// /// The code point of the character. - /// - /// When this method returns, contains the glyphs for the given codepoint if the glyphs - /// are found; otherwise the default value. This parameter is passed uninitialized. + /// + /// When this method returns, contains the glyph for the given codepoint if the glyph + /// is found; otherwise the default value. This parameter is passed uninitialized. /// /// /// if the face contains glyphs for the specified codepoint; otherwise, . /// - public bool TryGetGlyphs(CodePoint codePoint, [NotNullWhen(true)] out IReadOnlyList? glyphs) - => this.TryGetGlyphs(codePoint, TextAttributes.None, ColorFontSupport.None, out glyphs); + public bool TryGetGlyphs(CodePoint codePoint, [NotNullWhen(true)] out Glyph? glyph) + => this.TryGetGlyphs(codePoint, TextAttributes.None, ColorFontSupport.None, out glyph); /// - /// Gets the glyphs for the given codepoint. + /// Gets the glyph for the given codepoint. /// /// The code point of the character. /// Options for enabling color font support during layout and rendering. - /// - /// When this method returns, contains the glyphs for the given codepoint and color support if the glyphs - /// are found; otherwise the default value. This parameter is passed uninitialized. + /// + /// When this method returns, contains the glyphs for the given codepoint and color support if the glyph + /// is found; otherwise the default value. This parameter is passed uninitialized. /// /// /// if the face contains glyphs for the specified codepoint; otherwise, . /// - public bool TryGetGlyphs(CodePoint codePoint, ColorFontSupport support, [NotNullWhen(true)] out IReadOnlyList? glyphs) - => this.TryGetGlyphs(codePoint, TextAttributes.None, support, out glyphs); + public bool TryGetGlyphs(CodePoint codePoint, ColorFontSupport support, [NotNullWhen(true)] out Glyph? glyph) + => this.TryGetGlyphs(codePoint, TextAttributes.None, support, out glyph); /// - /// Gets the glyphs for the given codepoint. + /// Gets the glyph for the given codepoint. /// /// The code point of the character. /// The text attributes to apply to the glyphs. /// Options for enabling color font support during layout and rendering. - /// - /// When this method returns, contains the glyphs for the given codepoint, attributes, and color support if the glyphs - /// are found; otherwise the default value. This parameter is passed uninitialized. + /// + /// When this method returns, contains the glyph for the given codepoint, attributes, and color support if the glyph + /// is found; otherwise the default value. This parameter is passed uninitialized. /// /// /// if the face contains glyphs for the specified codepoint; otherwise, . @@ -187,68 +187,62 @@ public bool TryGetGlyphs( CodePoint codePoint, TextAttributes textAttributes, ColorFontSupport support, - [NotNullWhen(true)] out IReadOnlyList? glyphs) - => this.TryGetGlyphs(codePoint, textAttributes, TextDecorations.None, LayoutMode.HorizontalTopBottom, support, out glyphs); + [NotNullWhen(true)] out Glyph? glyph) + => this.TryGetGlyph(codePoint, textAttributes, TextDecorations.None, LayoutMode.HorizontalTopBottom, support, out glyph); /// - /// Gets the glyphs for the given codepoint. + /// Gets the glyph for the given codepoint. /// /// The code point of the character. /// The text attributes to apply to the glyphs. /// The layout mode to apply to the glyphs. /// Options for enabling color font support during layout and rendering. - /// - /// When this method returns, contains the glyphs for the given codepoint, attributes, and color support if the glyphs - /// are found; otherwise the default value. This parameter is passed uninitialized. + /// + /// When this method returns, contains the glyph for the given codepoint, attributes, and color support if the glyph + /// is found; otherwise the default value. This parameter is passed uninitialized. /// /// /// if the face contains glyphs for the specified codepoint; otherwise, . /// - public bool TryGetGlyphs( + public bool TryGetGlyph( CodePoint codePoint, TextAttributes textAttributes, LayoutMode layoutMode, ColorFontSupport support, - [NotNullWhen(true)] out IReadOnlyList? glyphs) - => this.TryGetGlyphs(codePoint, textAttributes, TextDecorations.None, layoutMode, support, out glyphs); + [NotNullWhen(true)] out Glyph? glyph) + => this.TryGetGlyph(codePoint, textAttributes, TextDecorations.None, layoutMode, support, out glyph); /// - /// Gets the glyphs for the given codepoint. + /// Gets the glyph for the given codepoint. /// /// The code point of the character. /// The text attributes to apply to the glyphs. /// The text decorations to apply to the glyphs. /// The layout mode to apply to the glyphs. /// Options for enabling color font support during layout and rendering. - /// - /// When this method returns, contains the glyphs for the given codepoint, attributes, and color support if the glyphs - /// are found; otherwise the default value. This parameter is passed uninitialized. + /// + /// When this method returns, contains the glyph for the given codepoint, attributes, and color support if the glyph + /// is found; otherwise the default value. This parameter is passed uninitialized. /// /// /// if the face contains glyphs for the specified codepoint; otherwise, . /// - public bool TryGetGlyphs( + public bool TryGetGlyph( CodePoint codePoint, TextAttributes textAttributes, TextDecorations textDecorations, LayoutMode layoutMode, ColorFontSupport support, - [NotNullWhen(true)] out IReadOnlyList? glyphs) + [NotNullWhen(true)] out Glyph? glyph) { TextRun textRun = new() { Start = 0, End = 1, Font = this, TextAttributes = textAttributes, TextDecorations = textDecorations }; - if (this.FontMetrics.TryGetGlyphMetrics(codePoint, textAttributes, textDecorations, layoutMode, support, out IReadOnlyList? metrics)) + if (this.FontMetrics.TryGetGlyphMetrics(codePoint, textAttributes, textDecorations, layoutMode, support, out GlyphMetrics? metrics)) { - List g = new(); - foreach (GlyphMetrics metric in metrics) - { - g.Add(new(metric.CloneForRendering(textRun), this.Size)); - } - - glyphs = g; + glyph = new(metrics.CloneForRendering(textRun), this.Size); return true; } - glyphs = default; + glyph = null; return false; } diff --git a/src/SixLabors.Fonts/FontCollection.cs b/src/SixLabors.Fonts/FontCollection.cs index 17f6f29c6..dbdd67ec9 100644 --- a/src/SixLabors.Fonts/FontCollection.cs +++ b/src/SixLabors.Fonts/FontCollection.cs @@ -78,11 +78,11 @@ public IEnumerable AddCollection(Stream stream, out IEnumerable public FontFamily Get(string name) - => this.Get(name, CultureInfo.InvariantCulture); + => this.GetByCulture(name, CultureInfo.InvariantCulture); /// public bool TryGet(string name, out FontFamily family) - => this.TryGet(name, CultureInfo.InvariantCulture, out family); + => this.TryGetByCulture(name, CultureInfo.InvariantCulture, out family); /// public FontFamily Add(string path, CultureInfo culture) @@ -127,11 +127,11 @@ public IEnumerable GetByCulture(CultureInfo culture) => this.FamiliesByCultureImpl(culture); /// - public FontFamily Get(string name, CultureInfo culture) + public FontFamily GetByCulture(string name, CultureInfo culture) => this.GetImpl(name, culture); /// - public bool TryGet(string name, CultureInfo culture, out FontFamily family) + public bool TryGetByCulture(string name, CultureInfo culture, out FontFamily family) => this.TryGetImpl(name, culture, out family); /// diff --git a/src/SixLabors.Fonts/FontMetrics.cs b/src/SixLabors.Fonts/FontMetrics.cs index d108f1fa3..ffd582f6e 100644 --- a/src/SixLabors.Fonts/FontMetrics.cs +++ b/src/SixLabors.Fonts/FontMetrics.cs @@ -193,7 +193,7 @@ public abstract bool TryGetGlyphMetrics( TextDecorations textDecorations, LayoutMode layoutMode, ColorFontSupport support, - [NotNullWhen(true)] out IReadOnlyList? metrics); + [NotNullWhen(true)] out GlyphMetrics? metrics); /// /// Gets the unicode codepoints for which a glyph exists in the font. @@ -212,15 +212,15 @@ public abstract bool TryGetGlyphMetrics( /// The text attributes applied to the glyph. /// The text decorations applied to the glyph. /// The layout mode applied to the glyph. - /// Options for enabling color font support during layout and rendering. + /// Options for enabling color font support during layout and rendering. /// The . - internal abstract IReadOnlyList GetGlyphMetrics( + internal abstract GlyphMetrics GetGlyphMetrics( CodePoint codePoint, ushort glyphId, TextAttributes textAttributes, TextDecorations textDecorations, LayoutMode layoutMode, - ColorFontSupport support); + ColorFontSupport colorSupport); /// /// Tries to get the GSUB table. diff --git a/src/SixLabors.Fonts/FontRectangle.cs b/src/SixLabors.Fonts/FontRectangle.cs index cc6f3391d..4f59b43d4 100644 --- a/src/SixLabors.Fonts/FontRectangle.cs +++ b/src/SixLabors.Fonts/FontRectangle.cs @@ -46,6 +46,19 @@ public FontRectangle(Vector2 point, Vector2 size) { } + /// + /// Initializes a new instance of the structure using the specified bounding box. + /// + /// The bounding box that defines the position and size of the rectangle. + internal FontRectangle(in Bounds bound) + { + this.X = bound.Min.X; + this.Y = bound.Min.Y; + Vector2 size = bound.Max - bound.Min; + this.Width = size.X; + this.Height = size.Y; + } + /// /// Gets the x-coordinate of this . /// diff --git a/src/SixLabors.Fonts/Glyph.cs b/src/SixLabors.Fonts/Glyph.cs index c89d9b0a4..c69247ec6 100644 --- a/src/SixLabors.Fonts/Glyph.cs +++ b/src/SixLabors.Fonts/Glyph.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Numerics; +using SixLabors.Fonts.Rendering; namespace SixLabors.Fonts; @@ -37,10 +38,11 @@ public FontRectangle BoundingBox(GlyphLayoutMode mode, Vector2 location, float d /// Renders the glyph to the render surface relative to a top left origin. /// /// The surface. + /// The index of the grapheme this glyph is part of. /// The location to render the glyph at. /// The offset of the glyph vector relative to the top-left position of the glyph advance. /// The glyph layout mode to render using. /// The options to render using. - internal void RenderTo(IGlyphRenderer surface, Vector2 location, Vector2 offset, GlyphLayoutMode mode, TextOptions options) - => this.GlyphMetrics.RenderTo(surface, location, offset, mode, options); + internal void RenderTo(IGlyphRenderer surface, int graphemeIndex, Vector2 location, Vector2 offset, GlyphLayoutMode mode, TextOptions options) + => this.GlyphMetrics.RenderTo(surface, graphemeIndex, location, offset, mode, options); } diff --git a/src/SixLabors.Fonts/GlyphColor.KnownColors.cs b/src/SixLabors.Fonts/GlyphColor.KnownColors.cs new file mode 100644 index 000000000..3e4625ce3 --- /dev/null +++ b/src/SixLabors.Fonts/GlyphColor.KnownColors.cs @@ -0,0 +1,912 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts; + +/// +/// Contains static named color values. +/// +/// +public readonly partial struct GlyphColor +{ + private static readonly Lazy> NamedGlyphColorsLookupLazy = new(CreateNamedGlyphColorsLookup, true); + + /// + /// Represents a matching the W3C definition that has an hex value of #F0F8FF. + /// + public static readonly GlyphColor AliceBlue = new(240, 248, 255, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #FAEBD7. + /// + public static readonly GlyphColor AntiqueWhite = new(250, 235, 215, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #00FFFF. + /// + public static readonly GlyphColor Aqua = new(0, 255, 255, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #7FFFD4. + /// + public static readonly GlyphColor Aquamarine = new(127, 255, 212, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #F0FFFF. + /// + public static readonly GlyphColor Azure = new(240, 255, 255, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #F5F5DC. + /// + public static readonly GlyphColor Beige = new(245, 245, 220, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #FFE4C4. + /// + public static readonly GlyphColor Bisque = new(255, 228, 196, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #000000. + /// + public static readonly GlyphColor Black = new(0, 0, 0, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #FFEBCD. + /// + public static readonly GlyphColor BlanchedAlmond = new(255, 235, 205, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #0000FF. + /// + public static readonly GlyphColor Blue = new(0, 0, 255, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #8A2BE2. + /// + public static readonly GlyphColor BlueViolet = new(138, 43, 226, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #A52A2A. + /// + public static readonly GlyphColor Brown = new(165, 42, 42, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #DEB887. + /// + public static readonly GlyphColor BurlyWood = new(222, 184, 135, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #5F9EA0. + /// + public static readonly GlyphColor CadetBlue = new(95, 158, 160, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #7FFF00. + /// + public static readonly GlyphColor Chartreuse = new(127, 255, 0, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #D2691E. + /// + public static readonly GlyphColor Chocolate = new(210, 105, 30, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #FF7F50. + /// + public static readonly GlyphColor Coral = new(255, 127, 80, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #6495ED. + /// + public static readonly GlyphColor CornflowerBlue = new(100, 149, 237, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #FFF8DC. + /// + public static readonly GlyphColor Cornsilk = new(255, 248, 220, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #DC143C. + /// + public static readonly GlyphColor Crimson = new(220, 20, 60, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #00FFFF. + /// + public static readonly GlyphColor Cyan = Aqua; + + /// + /// Represents a matching the W3C definition that has an hex value of #00008B. + /// + public static readonly GlyphColor DarkBlue = new(0, 0, 139, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #008B8B. + /// + public static readonly GlyphColor DarkCyan = new(0, 139, 139, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #B8860B. + /// + public static readonly GlyphColor DarkGoldenrod = new(184, 134, 11, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #A9A9A9. + /// + public static readonly GlyphColor DarkGray = new(169, 169, 169, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #006400. + /// + public static readonly GlyphColor DarkGreen = new(0, 100, 0, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #A9A9A9. + /// + public static readonly GlyphColor DarkGrey = DarkGray; + + /// + /// Represents a matching the W3C definition that has an hex value of #BDB76B. + /// + public static readonly GlyphColor DarkKhaki = new(189, 183, 107, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #8B008B. + /// + public static readonly GlyphColor DarkMagenta = new(139, 0, 139, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #556B2F. + /// + public static readonly GlyphColor DarkOliveGreen = new(85, 107, 47, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #FF8C00. + /// + public static readonly GlyphColor DarkOrange = new(255, 140, 0, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #9932CC. + /// + public static readonly GlyphColor DarkOrchid = new(153, 50, 204, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #8B0000. + /// + public static readonly GlyphColor DarkRed = new(139, 0, 0, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #E9967A. + /// + public static readonly GlyphColor DarkSalmon = new(233, 150, 122, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #8FBC8F. + /// + public static readonly GlyphColor DarkSeaGreen = new(143, 188, 143, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #483D8B. + /// + public static readonly GlyphColor DarkSlateBlue = new(72, 61, 139, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #2F4F4F. + /// + public static readonly GlyphColor DarkSlateGray = new(47, 79, 79, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #2F4F4F. + /// + public static readonly GlyphColor DarkSlateGrey = DarkSlateGray; + + /// + /// Represents a matching the W3C definition that has an hex value of #00CED1. + /// + public static readonly GlyphColor DarkTurquoise = new(0, 206, 209, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #9400D3. + /// + public static readonly GlyphColor DarkViolet = new(148, 0, 211, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #FF1493. + /// + public static readonly GlyphColor DeepPink = new(255, 20, 147, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #00BFFF. + /// + public static readonly GlyphColor DeepSkyBlue = new(0, 191, 255, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #696969. + /// + public static readonly GlyphColor DimGray = new(105, 105, 105, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #696969. + /// + public static readonly GlyphColor DimGrey = DimGray; + + /// + /// Represents a matching the W3C definition that has an hex value of #1E90FF. + /// + public static readonly GlyphColor DodgerBlue = new(30, 144, 255, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #B22222. + /// + public static readonly GlyphColor Firebrick = new(178, 34, 34, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #FFFAF0. + /// + public static readonly GlyphColor FloralWhite = new(255, 250, 240, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #228B22. + /// + public static readonly GlyphColor ForestGreen = new(34, 139, 34, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #FF00FF. + /// + public static readonly GlyphColor Fuchsia = new(255, 0, 255, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #DCDCDC. + /// + public static readonly GlyphColor Gainsboro = new(220, 220, 220, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #F8F8FF. + /// + public static readonly GlyphColor GhostWhite = new(248, 248, 255, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #FFD700. + /// + public static readonly GlyphColor Gold = new(255, 215, 0, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #DAA520. + /// + public static readonly GlyphColor Goldenrod = new(218, 165, 32, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #808080. + /// + public static readonly GlyphColor Gray = new(128, 128, 128, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #008000. + /// + public static readonly GlyphColor Green = new(0, 128, 0, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #ADFF2F. + /// + public static readonly GlyphColor GreenYellow = new(173, 255, 47, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #808080. + /// + public static readonly GlyphColor Grey = Gray; + + /// + /// Represents a matching the W3C definition that has an hex value of #F0FFF0. + /// + public static readonly GlyphColor Honeydew = new(240, 255, 240, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #FF69B4. + /// + public static readonly GlyphColor HotPink = new(255, 105, 180, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #CD5C5C. + /// + public static readonly GlyphColor IndianRed = new(205, 92, 92, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #4B0082. + /// + public static readonly GlyphColor Indigo = new(75, 0, 130, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #FFFFF0. + /// + public static readonly GlyphColor Ivory = new(255, 255, 240, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #F0E68C. + /// + public static readonly GlyphColor Khaki = new(240, 230, 140, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #E6E6FA. + /// + public static readonly GlyphColor Lavender = new(230, 230, 250, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #FFF0F5. + /// + public static readonly GlyphColor LavenderBlush = new(255, 240, 245, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #7CFC00. + /// + public static readonly GlyphColor LawnGreen = new(124, 252, 0, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #FFFACD. + /// + public static readonly GlyphColor LemonChiffon = new(255, 250, 205, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #ADD8E6. + /// + public static readonly GlyphColor LightBlue = new(173, 216, 230, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #F08080. + /// + public static readonly GlyphColor LightCoral = new(240, 128, 128, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #E0FFFF. + /// + public static readonly GlyphColor LightCyan = new(224, 255, 255, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #FAFAD2. + /// + public static readonly GlyphColor LightGoldenrodYellow = new(250, 250, 210, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #D3D3D3. + /// + public static readonly GlyphColor LightGray = new(211, 211, 211, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #90EE90. + /// + public static readonly GlyphColor LightGreen = new(144, 238, 144, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #D3D3D3. + /// + public static readonly GlyphColor LightGrey = LightGray; + + /// + /// Represents a matching the W3C definition that has an hex value of #FFB6C1. + /// + public static readonly GlyphColor LightPink = new(255, 182, 193, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #FFA07A. + /// + public static readonly GlyphColor LightSalmon = new(255, 160, 122, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #20B2AA. + /// + public static readonly GlyphColor LightSeaGreen = new(32, 178, 170, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #87CEFA. + /// + public static readonly GlyphColor LightSkyBlue = new(135, 206, 250, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #778899. + /// + public static readonly GlyphColor LightSlateGray = new(119, 136, 153, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #778899. + /// + public static readonly GlyphColor LightSlateGrey = LightSlateGray; + + /// + /// Represents a matching the W3C definition that has an hex value of #B0C4DE. + /// + public static readonly GlyphColor LightSteelBlue = new(176, 196, 222, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #FFFFE0. + /// + public static readonly GlyphColor LightYellow = new(255, 255, 224, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #00FF00. + /// + public static readonly GlyphColor Lime = new(0, 255, 0, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #32CD32. + /// + public static readonly GlyphColor LimeGreen = new(50, 205, 50, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #FAF0E6. + /// + public static readonly GlyphColor Linen = new(250, 240, 230, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #FF00FF. + /// + public static readonly GlyphColor Magenta = Fuchsia; + + /// + /// Represents a matching the W3C definition that has an hex value of #800000. + /// + public static readonly GlyphColor Maroon = new(128, 0, 0, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #66CDAA. + /// + public static readonly GlyphColor MediumAquamarine = new(102, 205, 170, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #0000CD. + /// + public static readonly GlyphColor MediumBlue = new(0, 0, 205, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #BA55D3. + /// + public static readonly GlyphColor MediumOrchid = new(186, 85, 211, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #9370DB. + /// + public static readonly GlyphColor MediumPurple = new(147, 112, 219, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #3CB371. + /// + public static readonly GlyphColor MediumSeaGreen = new(60, 179, 113, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #7B68EE. + /// + public static readonly GlyphColor MediumSlateBlue = new(123, 104, 238, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #00FA9A. + /// + public static readonly GlyphColor MediumSpringGreen = new(0, 250, 154, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #48D1CC. + /// + public static readonly GlyphColor MediumTurquoise = new(72, 209, 204, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #C71585. + /// + public static readonly GlyphColor MediumVioletRed = new(199, 21, 133, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #191970. + /// + public static readonly GlyphColor MidnightBlue = new(25, 25, 112, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #F5FFFA. + /// + public static readonly GlyphColor MintCream = new(245, 255, 250, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #FFE4E1. + /// + public static readonly GlyphColor MistyRose = new(255, 228, 225, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #FFE4B5. + /// + public static readonly GlyphColor Moccasin = new(255, 228, 181, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #FFDEAD. + /// + public static readonly GlyphColor NavajoWhite = new(255, 222, 173, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #000080. + /// + public static readonly GlyphColor Navy = new(0, 0, 128, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #FDF5E6. + /// + public static readonly GlyphColor OldLace = new(253, 245, 230, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #808000. + /// + public static readonly GlyphColor Olive = new(128, 128, 0, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #6B8E23. + /// + public static readonly GlyphColor OliveDrab = new(107, 142, 35, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #FFA500. + /// + public static readonly GlyphColor Orange = new(255, 165, 0, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #FF4500. + /// + public static readonly GlyphColor OrangeRed = new(255, 69, 0, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #DA70D6. + /// + public static readonly GlyphColor Orchid = new(218, 112, 214, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #EEE8AA. + /// + public static readonly GlyphColor PaleGoldenrod = new(238, 232, 170, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #98FB98. + /// + public static readonly GlyphColor PaleGreen = new(152, 251, 152, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #AFEEEE. + /// + public static readonly GlyphColor PaleTurquoise = new(175, 238, 238, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #DB7093. + /// + public static readonly GlyphColor PaleVioletRed = new(219, 112, 147, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #FFEFD5. + /// + public static readonly GlyphColor PapayaWhip = new(255, 239, 213, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #FFDAB9. + /// + public static readonly GlyphColor PeachPuff = new(255, 218, 185, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #CD853F. + /// + public static readonly GlyphColor Peru = new(205, 133, 63, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #FFC0CB. + /// + public static readonly GlyphColor Pink = new(255, 192, 203, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #DDA0DD. + /// + public static readonly GlyphColor Plum = new(221, 160, 221, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #B0E0E6. + /// + public static readonly GlyphColor PowderBlue = new(176, 224, 230, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #800080. + /// + public static readonly GlyphColor Purple = new(128, 0, 128, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #663399. + /// + public static readonly GlyphColor RebeccaPurple = new(102, 51, 153, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #FF0000. + /// + public static readonly GlyphColor Red = new(255, 0, 0, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #BC8F8F. + /// + public static readonly GlyphColor RosyBrown = new(188, 143, 143, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #4169E1. + /// + public static readonly GlyphColor RoyalBlue = new(65, 105, 225, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #8B4513. + /// + public static readonly GlyphColor SaddleBrown = new(139, 69, 19, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #FA8072. + /// + public static readonly GlyphColor Salmon = new(250, 128, 114, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #F4A460. + /// + public static readonly GlyphColor SandyBrown = new(244, 164, 96, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #2E8B57. + /// + public static readonly GlyphColor SeaGreen = new(46, 139, 87, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #FFF5EE. + /// + public static readonly GlyphColor SeaShell = new(255, 245, 238, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #A0522D. + /// + public static readonly GlyphColor Sienna = new(160, 82, 45, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #C0C0C0. + /// + public static readonly GlyphColor Silver = new(192, 192, 192, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #87CEEB. + /// + public static readonly GlyphColor SkyBlue = new(135, 206, 235, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #6A5ACD. + /// + public static readonly GlyphColor SlateBlue = new(106, 90, 205, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #708090. + /// + public static readonly GlyphColor SlateGray = new(112, 128, 144, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #708090. + /// + public static readonly GlyphColor SlateGrey = SlateGray; + + /// + /// Represents a matching the W3C definition that has an hex value of #FFFAFA. + /// + public static readonly GlyphColor Snow = new(255, 250, 250, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #00FF7F. + /// + public static readonly GlyphColor SpringGreen = new(0, 255, 127, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #4682B4. + /// + public static readonly GlyphColor SteelBlue = new(70, 130, 180, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #D2B48C. + /// + public static readonly GlyphColor Tan = new(210, 180, 140, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #008080. + /// + public static readonly GlyphColor Teal = new(0, 128, 128, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #D8BFD8. + /// + public static readonly GlyphColor Thistle = new(216, 191, 216, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #FF6347. + /// + public static readonly GlyphColor Tomato = new(255, 99, 71, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #00000000. + /// + public static readonly GlyphColor Transparent = new(0, 0, 0, 0); + + /// + /// Represents a matching the W3C definition that has an hex value of #40E0D0. + /// + public static readonly GlyphColor Turquoise = new(64, 224, 208, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #EE82EE. + /// + public static readonly GlyphColor Violet = new(238, 130, 238, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #F5DEB3. + /// + public static readonly GlyphColor Wheat = new(245, 222, 179, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #FFFFFF. + /// + public static readonly GlyphColor White = new(255, 255, 255, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #F5F5F5. + /// + public static readonly GlyphColor WhiteSmoke = new(245, 245, 245, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #FFFF00. + /// + public static readonly GlyphColor Yellow = new(255, 255, 0, 255); + + /// + /// Represents a matching the W3C definition that has an hex value of #9ACD32. + /// + public static readonly GlyphColor YellowGreen = new(154, 205, 50, 255); + + private static Dictionary CreateNamedGlyphColorsLookup() + => new(StringComparer.OrdinalIgnoreCase) + { + { nameof(AliceBlue), AliceBlue }, + { nameof(AntiqueWhite), AntiqueWhite }, + { nameof(Aqua), Aqua }, + { nameof(Aquamarine), Aquamarine }, + { nameof(Azure), Azure }, + { nameof(Beige), Beige }, + { nameof(Bisque), Bisque }, + { nameof(Black), Black }, + { nameof(BlanchedAlmond), BlanchedAlmond }, + { nameof(Blue), Blue }, + { nameof(BlueViolet), BlueViolet }, + { nameof(Brown), Brown }, + { nameof(BurlyWood), BurlyWood }, + { nameof(CadetBlue), CadetBlue }, + { nameof(Chartreuse), Chartreuse }, + { nameof(Chocolate), Chocolate }, + { nameof(Coral), Coral }, + { nameof(CornflowerBlue), CornflowerBlue }, + { nameof(Cornsilk), Cornsilk }, + { nameof(Crimson), Crimson }, + { nameof(Cyan), Cyan }, + { nameof(DarkBlue), DarkBlue }, + { nameof(DarkCyan), DarkCyan }, + { nameof(DarkGoldenrod), DarkGoldenrod }, + { nameof(DarkGray), DarkGray }, + { nameof(DarkGreen), DarkGreen }, + { nameof(DarkGrey), DarkGrey }, + { nameof(DarkKhaki), DarkKhaki }, + { nameof(DarkMagenta), DarkMagenta }, + { nameof(DarkOliveGreen), DarkOliveGreen }, + { nameof(DarkOrange), DarkOrange }, + { nameof(DarkOrchid), DarkOrchid }, + { nameof(DarkRed), DarkRed }, + { nameof(DarkSalmon), DarkSalmon }, + { nameof(DarkSeaGreen), DarkSeaGreen }, + { nameof(DarkSlateBlue), DarkSlateBlue }, + { nameof(DarkSlateGray), DarkSlateGray }, + { nameof(DarkSlateGrey), DarkSlateGrey }, + { nameof(DarkTurquoise), DarkTurquoise }, + { nameof(DarkViolet), DarkViolet }, + { nameof(DeepPink), DeepPink }, + { nameof(DeepSkyBlue), DeepSkyBlue }, + { nameof(DimGray), DimGray }, + { nameof(DimGrey), DimGrey }, + { nameof(DodgerBlue), DodgerBlue }, + { nameof(Firebrick), Firebrick }, + { nameof(FloralWhite), FloralWhite }, + { nameof(ForestGreen), ForestGreen }, + { nameof(Fuchsia), Fuchsia }, + { nameof(Gainsboro), Gainsboro }, + { nameof(GhostWhite), GhostWhite }, + { nameof(Gold), Gold }, + { nameof(Goldenrod), Goldenrod }, + { nameof(Gray), Gray }, + { nameof(Green), Green }, + { nameof(GreenYellow), GreenYellow }, + { nameof(Grey), Grey }, + { nameof(Honeydew), Honeydew }, + { nameof(HotPink), HotPink }, + { nameof(IndianRed), IndianRed }, + { nameof(Indigo), Indigo }, + { nameof(Ivory), Ivory }, + { nameof(Khaki), Khaki }, + { nameof(Lavender), Lavender }, + { nameof(LavenderBlush), LavenderBlush }, + { nameof(LawnGreen), LawnGreen }, + { nameof(LemonChiffon), LemonChiffon }, + { nameof(LightBlue), LightBlue }, + { nameof(LightCoral), LightCoral }, + { nameof(LightCyan), LightCyan }, + { nameof(LightGoldenrodYellow), LightGoldenrodYellow }, + { nameof(LightGray), LightGray }, + { nameof(LightGreen), LightGreen }, + { nameof(LightGrey), LightGrey }, + { nameof(LightPink), LightPink }, + { nameof(LightSalmon), LightSalmon }, + { nameof(LightSeaGreen), LightSeaGreen }, + { nameof(LightSkyBlue), LightSkyBlue }, + { nameof(LightSlateGray), LightSlateGray }, + { nameof(LightSlateGrey), LightSlateGrey }, + { nameof(LightSteelBlue), LightSteelBlue }, + { nameof(LightYellow), LightYellow }, + { nameof(Lime), Lime }, + { nameof(LimeGreen), LimeGreen }, + { nameof(Linen), Linen }, + { nameof(Magenta), Magenta }, + { nameof(Maroon), Maroon }, + { nameof(MediumAquamarine), MediumAquamarine }, + { nameof(MediumBlue), MediumBlue }, + { nameof(MediumOrchid), MediumOrchid }, + { nameof(MediumPurple), MediumPurple }, + { nameof(MediumSeaGreen), MediumSeaGreen }, + { nameof(MediumSlateBlue), MediumSlateBlue }, + { nameof(MediumSpringGreen), MediumSpringGreen }, + { nameof(MediumTurquoise), MediumTurquoise }, + { nameof(MediumVioletRed), MediumVioletRed }, + { nameof(MidnightBlue), MidnightBlue }, + { nameof(MintCream), MintCream }, + { nameof(MistyRose), MistyRose }, + { nameof(Moccasin), Moccasin }, + { nameof(NavajoWhite), NavajoWhite }, + { nameof(Navy), Navy }, + { nameof(OldLace), OldLace }, + { nameof(Olive), Olive }, + { nameof(OliveDrab), OliveDrab }, + { nameof(Orange), Orange }, + { nameof(OrangeRed), OrangeRed }, + { nameof(Orchid), Orchid }, + { nameof(PaleGoldenrod), PaleGoldenrod }, + { nameof(PaleGreen), PaleGreen }, + { nameof(PaleTurquoise), PaleTurquoise }, + { nameof(PaleVioletRed), PaleVioletRed }, + { nameof(PapayaWhip), PapayaWhip }, + { nameof(PeachPuff), PeachPuff }, + { nameof(Peru), Peru }, + { nameof(Pink), Pink }, + { nameof(Plum), Plum }, + { nameof(PowderBlue), PowderBlue }, + { nameof(Purple), Purple }, + { nameof(RebeccaPurple), RebeccaPurple }, + { nameof(Red), Red }, + { nameof(RosyBrown), RosyBrown }, + { nameof(RoyalBlue), RoyalBlue }, + { nameof(SaddleBrown), SaddleBrown }, + { nameof(Salmon), Salmon }, + { nameof(SandyBrown), SandyBrown }, + { nameof(SeaGreen), SeaGreen }, + { nameof(SeaShell), SeaShell }, + { nameof(Sienna), Sienna }, + { nameof(Silver), Silver }, + { nameof(SkyBlue), SkyBlue }, + { nameof(SlateBlue), SlateBlue }, + { nameof(SlateGray), SlateGray }, + { nameof(SlateGrey), SlateGrey }, + { nameof(Snow), Snow }, + { nameof(SpringGreen), SpringGreen }, + { nameof(SteelBlue), SteelBlue }, + { nameof(Tan), Tan }, + { nameof(Teal), Teal }, + { nameof(Thistle), Thistle }, + { nameof(Tomato), Tomato }, + { nameof(Transparent), Transparent }, + { nameof(Turquoise), Turquoise }, + { nameof(Violet), Violet }, + { nameof(Wheat), Wheat }, + { nameof(White), White }, + { nameof(WhiteSmoke), WhiteSmoke }, + { nameof(Yellow), Yellow }, + { nameof(YellowGreen), YellowGreen } + }; +} diff --git a/src/SixLabors.Fonts/GlyphColor.cs b/src/SixLabors.Fonts/GlyphColor.cs new file mode 100644 index 000000000..2bff88d7c --- /dev/null +++ b/src/SixLabors.Fonts/GlyphColor.cs @@ -0,0 +1,251 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace SixLabors.Fonts; + +/// +/// Provides access to the color details for the current glyph. +/// +public readonly partial struct GlyphColor : IEquatable +{ + internal GlyphColor(byte red, byte green, byte blue, byte alpha) + { + this.R = red; + this.G = green; + this.B = blue; + this.A = alpha; + } + + /// + /// Gets the red component + /// + public readonly byte R { get; } + + /// + /// Gets the green component + /// + public readonly byte G { get; } + + /// + /// Gets the blue component + /// + public readonly byte B { get; } + + /// + /// Gets the alpha component + /// + public readonly byte A { get; } + + /// + /// Compares two objects for equality. + /// + /// + /// The on the left side of the operand. + /// + /// + /// The on the right side of the operand. + /// + /// + /// True if the current left is equal to the parameter; otherwise, false. + /// + public static bool operator ==(GlyphColor left, GlyphColor right) + => left.Equals(right); + + /// + /// Compares two objects for inequality. + /// + /// + /// The on the left side of the operand. + /// + /// + /// The on the right side of the operand. + /// + /// + /// True if the current left is unequal to the parameter; otherwise, false. + /// + public static bool operator !=(GlyphColor left, GlyphColor right) + => !left.Equals(right); + + /// + public override bool Equals(object? obj) + => obj is GlyphColor p && this.Equals(p); + + /// + /// Compares the for equality to this color. + /// + /// + /// The other to compare to. + /// + /// + /// True if the current color is equal to the parameter; otherwise, false. + /// + public bool Equals(GlyphColor other) + => other.R == this.R + && other.G == this.G + && other.B == this.B + && other.A == this.A; + + /// + public override int GetHashCode() + => HashCode.Combine( + this.R, + this.G, + this.B, + this.A); + + /// + /// Gets the hexadecimal string representation of the color instance in the format RRGGBBAA. + /// + /// + /// The hexadecimal representation of the combined color components. + /// + /// + /// When this method returns, contains the equivalent of the hexadecimal input. + /// + /// + /// if the parsing was successful; otherwise, . + /// + public static bool TryParseHex(string? value, [NotNullWhen(true)] out GlyphColor result) + { + result = default; + + if (string.IsNullOrWhiteSpace(value)) + { + return false; + } + + ReadOnlySpan hex = value.AsSpan(); + + if (hex[0] != '#') + { + return false; + } + + hex = hex[1..]; + + byte a = 255, r, g, b; + + switch (hex.Length) + { + case 8: + if (!TryParseByte(hex[0], hex[1], out r) || + !TryParseByte(hex[2], hex[3], out g) || + !TryParseByte(hex[4], hex[5], out b) || + !TryParseByte(hex[6], hex[7], out a)) + { + return false; + } + + break; + + case 6: + if (!TryParseByte(hex[0], hex[1], out r) || + !TryParseByte(hex[2], hex[3], out g) || + !TryParseByte(hex[4], hex[5], out b)) + { + return false; + } + + break; + + case 4: + if (!TryExpand(hex[0], out r) || + !TryExpand(hex[1], out g) || + !TryExpand(hex[2], out b) || + !TryExpand(hex[3], out a)) + { + return false; + } + + break; + + case 3: + if (!TryExpand(hex[0], out r) || + !TryExpand(hex[1], out g) || + !TryExpand(hex[2], out b)) + { + return false; + } + + break; + + default: + return false; + } + + result = new GlyphColor(r, g, b, a); + return true; + } + + /// + /// Attempts to parse the specified name into a corresponding named glyph color. + /// + /// The name of the glyph color to parse. + /// + /// When this method returns, contains the parsed value if the parse operation succeeded; + /// otherwise, contains the default value. + /// + /// + /// if the parsing was successful; otherwise, . + /// + public static bool TryParseNamed(string? name, [NotNullWhen(true)] out GlyphColor result) + { + result = default; + if (string.IsNullOrWhiteSpace(name)) + { + return false; + } + + return NamedGlyphColorsLookupLazy.Value.TryGetValue(name, out result); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool TryParseByte(char hi, char lo, out byte value) + { + if (TryConvertHexCharToByte(hi, out byte high) && TryConvertHexCharToByte(lo, out byte low)) + { + value = (byte)((high << 4) | low); + return true; + } + + value = 0; + return false; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool TryExpand(char c, out byte value) + { + if (TryConvertHexCharToByte(c, out byte nibble)) + { + value = (byte)((nibble << 4) | nibble); + return true; + } + + value = 0; + return false; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool TryConvertHexCharToByte(char c, out byte value) + { + if ((uint)(c - '0') <= 9) + { + value = (byte)(c - '0'); + return true; + } + + char lower = (char)(c | 0x20); // Normalize to lowercase + + if ((uint)(lower - 'a') <= 5) + { + value = (byte)(lower - 'a' + 10); + return true; + } + + value = 0; + return false; + } +} diff --git a/src/SixLabors.Fonts/GlyphMetrics.cs b/src/SixLabors.Fonts/GlyphMetrics.cs index 582193480..ef37544de 100644 --- a/src/SixLabors.Fonts/GlyphMetrics.cs +++ b/src/SixLabors.Fonts/GlyphMetrics.cs @@ -3,6 +3,7 @@ using System.Numerics; using System.Runtime.CompilerServices; +using SixLabors.Fonts.Rendering; using SixLabors.Fonts.Tables.General; using SixLabors.Fonts.Unicode; @@ -27,8 +28,7 @@ internal GlyphMetrics( ushort unitsPerEM, TextAttributes textAttributes, TextDecorations textDecorations, - GlyphType glyphType = GlyphType.Standard, - GlyphColor? glyphColor = null) + GlyphType glyphType) { this.FontMetrics = font; this.GlyphId = glyphId; @@ -46,10 +46,10 @@ internal GlyphMetrics( this.TextAttributes = textAttributes; this.TextDecorations = textDecorations; this.GlyphType = glyphType; - this.GlyphColor = glyphColor; Vector2 offset = Vector2.Zero; Vector2 scaleFactor = new(unitsPerEM * 72F); + if ((textAttributes & TextAttributes.Subscript) == TextAttributes.Subscript) { float units = this.UnitsPerEm; @@ -80,8 +80,7 @@ internal GlyphMetrics( Vector2 offset, Vector2 scaleFactor, TextRun textRun, - GlyphType glyphType = GlyphType.Standard, - GlyphColor? glyphColor = null) + GlyphType glyphType) { // This is used during cloning. Ensure anything that could be changed is copied. this.FontMetrics = font; @@ -100,9 +99,8 @@ internal GlyphMetrics( this.TextAttributes = textRun.TextAttributes; this.TextDecorations = textRun.TextDecorations; this.GlyphType = glyphType; - this.GlyphColor = glyphColor; - this.ScaleFactor = new Vector2(scaleFactor.X, scaleFactor.Y); - this.Offset = new Vector2(offset.X, offset.Y); + this.ScaleFactor = scaleFactor; + this.Offset = offset; this.TextRun = textRun; } @@ -166,11 +164,6 @@ internal GlyphMetrics( /// public GlyphType GlyphType { get; } - /// - /// Gets the color of this glyph when the is - /// - public GlyphColor? GlyphColor { get; } - /// public ushort UnitsPerEm { get; } @@ -247,36 +240,68 @@ internal void ApplyAdvance(short x, short y) /// The y-advance. internal void SetAdvanceHeight(ushort y) => this.AdvanceHeight = y; + /// + /// Calculates the glyph bounding box in device-space (Y-down) coordinates, + /// given the layout mode, render origin, and scaled point size. + /// + /// + /// Steps: + /// 1) Select glyph bounds (or synthesize from advances if empty). + /// 2) Apply rotation if the layout mode is vertical-rotated. + /// 3) Convert from Y-up to Y-down coordinates. + /// 4) Scale and translate to device space using the specified origin. + /// + /// The glyph layout mode (horizontal, vertical, or vertical rotated). + /// The render-space origin in pixels. + /// The scaled point size, mapped to pixels by the caller. + /// + /// A representing the glyph bounds in device space. + /// internal FontRectangle GetBoundingBox(GlyphLayoutMode mode, Vector2 origin, float scaledPointSize) { - Vector2 scale = new Vector2(scaledPointSize) / this.ScaleFactor; - Bounds bounds = this.Bounds; + Vector2 scale = new(scaledPointSize / this.ScaleFactor.X, scaledPointSize / this.ScaleFactor.Y); + Bounds b = this.Bounds; - if (bounds.Equals(Bounds.Empty)) + // 1) Substitute fallback bounds if the glyph has no outline. + if (b.Equals(Bounds.Empty)) { - // For non-vertical layout, the advance width only is used to compute the bounding box - // as the advance height represents the maximum possible advance. - if (mode != GlyphLayoutMode.Vertical) + if (mode == GlyphLayoutMode.Vertical) { - bounds = new Bounds(0, 0, this.AdvanceWidth, 0); + // For vertical layout, set Y-up min = -AdvanceHeight to 0 so Y-down is 0..+AdvanceHeight. + b = new Bounds(0f, -this.AdvanceHeight, 0f, 0f); } else { - bounds = new Bounds(0, 0, 0, this.AdvanceHeight); + // For horizontal layout, just use advance width. + b = new Bounds(0f, 0f, this.AdvanceWidth, 0f); } } - // Rotate if required. + // 2) Rotate for vertical rotated layout. + Vector2 offsetUp = this.Offset; if (mode == GlyphLayoutMode.VerticalRotated) { - bounds = Bounds.Transform(in bounds, Matrix3x2.CreateRotation(-MathF.PI / 2F)); + Matrix3x2 rot = Matrix3x2.CreateRotation(-MathF.PI / 2F); + b = Bounds.Transform(in b, rot); + offsetUp = Vector2.Transform(offsetUp, rot); } - Vector2 size = bounds.Size() * scale; - Vector2 location = (new Vector2(bounds.Min.X, bounds.Min.Y) + this.Offset) * scale * YInverter; + // 3) Flip Y to convert to device-space (Y-down). + Vector2 minDown = b.Min * YInverter; + Vector2 maxDown = b.Max * YInverter; + Vector2 offsetDown = offsetUp * YInverter; + + // Normalize bounds after flipping. + float minX = MathF.Min(minDown.X, maxDown.X); + float maxX = MathF.Max(minDown.X, maxDown.X); + float minY = MathF.Min(minDown.Y, maxDown.Y); + float maxY = MathF.Max(minDown.Y, maxDown.Y); + + // 4) Apply scaling and origin translation. + Vector2 size = new(maxX - minX, maxY - minY); + size *= scale; + Vector2 location = origin + ((new Vector2(minX, minY) + offsetDown) * scale); - location -= new Vector2(0, size.Y); - location += origin; return new FontRectangle(location.X, location.Y, size.X, size.Y); } @@ -284,14 +309,75 @@ internal FontRectangle GetBoundingBox(GlyphLayoutMode mode, Vector2 origin, floa /// Renders the glyph to the render surface in font units relative to a bottom left origin at (0,0) /// /// The surface renderer. + /// The index of the grapheme this glyph is part of. /// The location representing offset of the glyph outer bounds relative to the origin. /// The offset of the glyph vector relative to the top-left position of the glyph advance. /// The glyph layout mode to render using. /// The options used to influence the rendering of this glyph. - internal abstract void RenderTo(IGlyphRenderer renderer, Vector2 location, Vector2 offset, GlyphLayoutMode mode, TextOptions options); + internal abstract void RenderTo(IGlyphRenderer renderer, int graphemeIndex, Vector2 location, Vector2 offset, GlyphLayoutMode mode, TextOptions options); - internal void RenderDecorationsTo(IGlyphRenderer renderer, Vector2 location, GlyphLayoutMode mode, Matrix3x2 transform, float scaledPPEM) + /// + /// Renders text decorations, such as underline, strikeout, and overline, for the current glyph to the specified + /// glyph renderer at the given location and layout mode. + /// + /// When rendering in vertical layout modes, decoration positions are synthesized to match common + /// typographic conventions. The renderer may override which decorations are enabled. Overline thickness is derived + /// from underline metrics if not explicitly specified. + /// The glyph renderer that receives the decoration drawing commands. + /// The position, in device-independent coordinates, where the decorations should be rendered relative to the glyph. + /// The layout mode that determines the orientation and positioning of the decorations (e.g., horizontal, vertical, + /// or vertical rotated). + /// The transformation matrix applied to the decoration coordinates before rendering. + /// The scaled pixels-per-em value used to adjust decoration size and positioning for the current rendering context. + /// Additional text rendering options that may influence decoration appearance or behavior. + protected void RenderDecorationsTo( + IGlyphRenderer renderer, + Vector2 location, + GlyphLayoutMode mode, + Matrix3x2 transform, + float scaledPPEM, + TextOptions options) { + bool perGlyph = options.DecorationPositioningMode == DecorationPositioningMode.GlyphFont; + FontMetrics fontMetrics = perGlyph + ? this.FontMetrics + : options.Font.FontMetrics; + + // The scale factor for the decoration length is treated separately from other factors + // as it is used to scale the length of the decoration line. + // This must always be derived from the glyph's own scale factor to ensure correct length. + Vector2 lengthScaleFactor = this.ScaleFactor; + + // These factors determine horizontal and vertical scaling and offset for the decorations. + // and are either per-glyph or derived from the common font metrics. + Vector2 scaleFactor; + Vector2 offset; + if (perGlyph) + { + // Use the pre-calculated values from this glyph. + scaleFactor = this.ScaleFactor; + offset = this.Offset; + } + else + { + // To ensure that we share the scaling when sharing font metrics we need to + // recalculate the offset and scale factor here using the common font metrics. + scaleFactor = new(fontMetrics.UnitsPerEm * 72F); + offset = Vector2.Zero; + if ((this.TextAttributes & TextAttributes.Subscript) == TextAttributes.Subscript) + { + float units = this.UnitsPerEm; + scaleFactor /= new Vector2(fontMetrics.SubscriptXSize / units, fontMetrics.SubscriptYSize / units); + offset = new(fontMetrics.SubscriptXOffset, fontMetrics.SubscriptYOffset < 0 ? fontMetrics.SubscriptYOffset : -fontMetrics.SubscriptYOffset); + } + else if ((this.TextAttributes & TextAttributes.Superscript) == TextAttributes.Superscript) + { + float units = this.UnitsPerEm; + scaleFactor /= new Vector2(fontMetrics.SuperscriptXSize / units, fontMetrics.SuperscriptYSize / units); + offset = new(fontMetrics.SuperscriptXOffset, fontMetrics.SuperscriptYOffset < 0 ? -fontMetrics.SuperscriptYOffset : fontMetrics.SuperscriptYOffset); + } + } + bool isVerticalLayout = mode is GlyphLayoutMode.Vertical or GlyphLayoutMode.VerticalRotated; (Vector2 Start, Vector2 End, float Thickness) GetEnds(TextDecorations decorations, float thickness, float decoratorPosition) { @@ -304,12 +390,13 @@ internal void RenderDecorationsTo(IGlyphRenderer renderer, Vector2 location, Gly return (Vector2.Zero, Vector2.Zero, 0); } - Vector2 scale = new Vector2(scaledPPEM) / this.ScaleFactor; + Vector2 lengthScale = new Vector2(scaledPPEM) / lengthScaleFactor; + Vector2 scale = new Vector2(scaledPPEM) / scaleFactor; // Undo the vertical offset applied when laying out the text. - Vector2 scaledOffset = (this.Offset + new Vector2(decoratorPosition, 0)) * scale; + Vector2 scaledOffset = (offset + new Vector2(decoratorPosition, 0)) * scale; - length *= scale.Y; + length *= lengthScale.Y; thickness *= scale.X; Vector2 tl = new(scaledOffset.X, scaledOffset.Y); @@ -342,10 +429,11 @@ internal void RenderDecorationsTo(IGlyphRenderer renderer, Vector2 location, Gly return (Vector2.Zero, Vector2.Zero, 0); } - Vector2 scale = new Vector2(scaledPPEM) / this.ScaleFactor; - Vector2 scaledOffset = (this.Offset + new Vector2(0, decoratorPosition)) * scale; + Vector2 lengthScale = new Vector2(scaledPPEM) / lengthScaleFactor; + Vector2 scale = new Vector2(scaledPPEM) / scaleFactor; + Vector2 scaledOffset = (offset + new Vector2(0, decoratorPosition)) * scale; - length *= scale.X; + length *= lengthScale.X; thickness *= scale.Y; Vector2 tl = new(scaledOffset.X, scaledOffset.Y); @@ -379,18 +467,18 @@ void SetDecoration(TextDecorations decorations, float thickness, float position) bool synthesized = mode == GlyphLayoutMode.Vertical; if ((decorations & TextDecorations.Underline) == TextDecorations.Underline) { - SetDecoration(TextDecorations.Underline, this.FontMetrics.UnderlineThickness, synthesized ? Math.Abs(this.FontMetrics.UnderlinePosition) : this.FontMetrics.UnderlinePosition); + SetDecoration(TextDecorations.Underline, fontMetrics.UnderlineThickness, synthesized ? Math.Abs(fontMetrics.UnderlinePosition) : fontMetrics.UnderlinePosition); } if ((decorations & TextDecorations.Strikeout) == TextDecorations.Strikeout) { - SetDecoration(TextDecorations.Strikeout, this.FontMetrics.StrikeoutSize, synthesized ? this.FontMetrics.UnitsPerEm * .5F : this.FontMetrics.StrikeoutPosition); + SetDecoration(TextDecorations.Strikeout, fontMetrics.StrikeoutSize, synthesized ? fontMetrics.UnitsPerEm * .5F : fontMetrics.StrikeoutPosition); } if ((decorations & TextDecorations.Overline) == TextDecorations.Overline) { // There's no built in metrics for overline thickness so use underline. - SetDecoration(TextDecorations.Overline, this.FontMetrics.UnderlineThickness, this.UnitsPerEm - this.FontMetrics.UnderlinePosition); + SetDecoration(TextDecorations.Overline, fontMetrics.UnderlineThickness, fontMetrics.UnitsPerEm - fontMetrics.UnderlinePosition); } } diff --git a/src/SixLabors.Fonts/GlyphPositioningCollection.cs b/src/SixLabors.Fonts/GlyphPositioningCollection.cs index ffb61a9fc..0094a2635 100644 --- a/src/SixLabors.Fonts/GlyphPositioningCollection.cs +++ b/src/SixLabors.Fonts/GlyphPositioningCollection.cs @@ -96,7 +96,7 @@ public bool TryGetGlyphMetricsAtOffset( out bool isDecomposed, [NotNullWhen(true)] out IReadOnlyList? metrics) { - List match = new(); + List match = []; pointSize = 0; isSubstituted = false; isVerticalSubstitution = false; @@ -122,7 +122,7 @@ public bool TryGetGlyphMetricsAtOffset( } pointSize = glyph.PointSize; - match.AddRange(glyph.Metrics); + match.Add(glyph.Metrics); } else if (match.Count > 0) { @@ -148,11 +148,11 @@ public bool TryUpdate(Font font, GlyphSubstitutionCollection collection) LayoutMode layoutMode = this.TextOptions.LayoutMode; ColorFontSupport colorFontSupport = this.TextOptions.ColorFontSupport; bool hasFallBacks = false; - List orphans = new(); + List orphans = []; for (int i = 0; i < this.glyphs.Count; i++) { GlyphPositioningData current = this.glyphs[i]; - if (current.Metrics[0].GlyphType != GlyphType.Fallback) + if (current.Metrics.GlyphType != GlyphType.Fallback) { // We've already got the correct glyph. continue; @@ -171,23 +171,19 @@ public bool TryUpdate(Font font, GlyphSubstitutionCollection collection) // Perform a semi-deep clone (FontMetrics is not cloned) so we can continue to // cache the original in the font metrics and only update our collection. - List metrics = new(data.Count); TextAttributes textAttributes = shape.TextRun.TextAttributes; TextDecorations textDecorations = shape.TextRun.TextDecorations; - foreach (GlyphMetrics gm in fontMetrics.GetGlyphMetrics(codePoint, id, textAttributes, textDecorations, layoutMode, colorFontSupport)) + GlyphMetrics metrics = fontMetrics.GetGlyphMetrics(codePoint, id, textAttributes, textDecorations, layoutMode, colorFontSupport); { - if (gm.GlyphType == GlyphType.Fallback && !CodePoint.IsControl(codePoint)) + // If the glyphs are fallbacks we don't want them as + // we've already captured them on the first run. + if (metrics.GlyphType == GlyphType.Fallback && !CodePoint.IsControl(codePoint)) { - // If the glyphs are fallbacks we don't want them as - // we've already captured them on the first run. hasFallBacks = true; - break; } - - metrics.Add(gm.CloneForRendering(shape.TextRun)); } - if (metrics.Count > 0) + if (!hasFallBacks) { if (j == 0) { @@ -196,15 +192,8 @@ public bool TryUpdate(Font font, GlyphSubstitutionCollection collection) } // Track the number of inserted glyphs at the offset so we can correctly increment our position. - ushort maxAdvancedWidth = 0; - ushort maxAdvancedHeight = 0; - for (int k = 0; k < metrics.Count; k++) - { - maxAdvancedWidth = Math.Max(maxAdvancedWidth, metrics[k].AdvanceWidth); - maxAdvancedHeight = Math.Max(maxAdvancedHeight, metrics[k].AdvanceHeight); - } - - this.glyphs.Insert(i += replacementCount, new(offset, new(shape, true) { Bounds = new(0, 0, maxAdvancedWidth, maxAdvancedHeight) }, pointSize, metrics.ToArray())); + GlyphShapingBounds bounds = new(0, 0, metrics.AdvanceWidth, metrics.AdvanceHeight); + this.glyphs.Insert(i += replacementCount, new(offset, new(shape, true) { Bounds = bounds }, pointSize, metrics.CloneForRendering(shape.TextRun))); replacementCount++; } } @@ -249,7 +238,6 @@ public bool TryAdd(Font font, GlyphSubstitutionCollection collection) GlyphShapingData data = collection.GetGlyphShapingData(i, out int offset); CodePoint codePoint = data.CodePoint; ushort id = data.GlyphId; - List metrics = new(); // Perform a semi-deep clone (FontMetrics is not cloned) so we can continue to // cache the original in the font metrics and only update our collection. @@ -264,28 +252,18 @@ public bool TryAdd(Font font, GlyphSubstitutionCollection collection) isVertical |= feature == vrtr; } - foreach (GlyphMetrics gm in fontMetrics.GetGlyphMetrics(codePoint, id, textAttributes, textDecorations, layoutMode, colorFontSupport)) - { - if (gm.GlyphType == GlyphType.Fallback && !CodePoint.IsControl(codePoint)) - { - hasFallBacks = true; - } - - metrics.Add(gm.CloneForRendering(data.TextRun)); - } + GlyphMetrics metrics = fontMetrics.GetGlyphMetrics(codePoint, id, textAttributes, textDecorations, layoutMode, colorFontSupport); - if (metrics.Count > 0) + if (metrics.GlyphType == GlyphType.Fallback && !CodePoint.IsControl(codePoint)) { - GlyphMetrics[] gm = metrics.ToArray(); - if (isVertical) - { - this.glyphs.Add(new(offset, new(data, true) { Bounds = new(0, 0, 0, gm[0].AdvanceHeight) }, font.Size, gm)); - } - else - { - this.glyphs.Add(new(offset, new(data, true) { Bounds = new(0, 0, gm[0].AdvanceWidth, 0) }, font.Size, gm)); - } + hasFallBacks = true; } + + GlyphShapingBounds bounds = isVertical + ? new(0, 0, 0, metrics.AdvanceHeight) + : new(0, 0, metrics.AdvanceWidth, 0); + + this.glyphs.Add(new(offset, new(data, true) { Bounds = bounds }, font.Size, metrics.CloneForRendering(data.TextRun))); } return !hasFallBacks; @@ -307,20 +285,19 @@ public void UpdatePosition(FontMetrics fontMetrics, int index) } ushort glyphId = data.GlyphId; - foreach (GlyphMetrics m in this.glyphs[index].Metrics) + GlyphMetrics m = this.glyphs[index].Metrics; + + if (m.GlyphId == glyphId && fontMetrics == m.FontMetrics) { - if (m.GlyphId == glyphId && fontMetrics == m.FontMetrics) + if (isDirtyXY) { - if (isDirtyXY) - { - m.ApplyOffset((short)data.Bounds.X, (short)data.Bounds.Y); - } + m.ApplyOffset((short)data.Bounds.X, (short)data.Bounds.Y); + } - if (isDirtyWH) - { - m.SetAdvanceWidth((ushort)data.Bounds.Width); - m.SetAdvanceHeight((ushort)data.Bounds.Height); - } + if (isDirtyWH) + { + m.SetAdvanceWidth((ushort)data.Bounds.Width); + m.SetAdvanceHeight((ushort)data.Bounds.Height); } } } @@ -342,21 +319,20 @@ public void Advance(FontMetrics fontMetrics, int index, ushort glyphId, short dx Tag vrtr = FeatureTags.VerticalAlternatesForRotation; GlyphPositioningData glyph = this.glyphs[index]; - foreach (GlyphMetrics m in glyph.Metrics) - { - if (m.GlyphId == glyphId && fontMetrics == m.FontMetrics) - { - bool isVertical = AdvancedTypographicUtils.IsVerticalGlyph(m.CodePoint, layoutMode); + GlyphMetrics m = glyph.Metrics; - foreach (Tag feature in glyph.Data.AppliedFeatures) - { - isVertical |= feature == vert; - isVertical |= feature == vrt2; - isVertical |= feature == vrtr; - } + if (m.GlyphId == glyphId && fontMetrics == m.FontMetrics) + { + bool isVertical = AdvancedTypographicUtils.IsVerticalGlyph(m.CodePoint, layoutMode); - m.ApplyAdvance(dx, isVertical ? dy : (short)0); + foreach (Tag feature in glyph.Data.AppliedFeatures) + { + isVertical |= feature == vert; + isVertical |= feature == vrt2; + isVertical |= feature == vrtr; } + + m.ApplyAdvance(dx, isVertical ? dy : (short)0); } } @@ -367,12 +343,12 @@ public void Advance(FontMetrics fontMetrics, int index, ushort glyphId, short dx /// The zero-based index of the elements to position. /// if the element should be processed; otherwise, . public bool ShouldProcess(FontMetrics fontMetrics, int index) - => this.glyphs[index].Metrics[0].FontMetrics == fontMetrics; + => this.glyphs[index].Metrics.FontMetrics == fontMetrics; [DebuggerDisplay("{DebuggerDisplay,nq}")] private class GlyphPositioningData { - public GlyphPositioningData(int offset, GlyphShapingData data, float pointSize, GlyphMetrics[] metrics) + public GlyphPositioningData(int offset, GlyphShapingData data, float pointSize, GlyphMetrics metrics) { this.Offset = offset; this.Data = data; @@ -386,7 +362,7 @@ public GlyphPositioningData(int offset, GlyphShapingData data, float pointSize, public float PointSize { get; set; } - public GlyphMetrics[] Metrics { get; set; } + public GlyphMetrics Metrics { get; set; } private string DebuggerDisplay => FormattableString.Invariant($"Offset: {this.Offset}, Data: {this.Data.ToDebuggerDisplay()}"); } diff --git a/src/SixLabors.Fonts/GlyphShapingData.cs b/src/SixLabors.Fonts/GlyphShapingData.cs index 8b7b2aab7..4e0e3e932 100644 --- a/src/SixLabors.Fonts/GlyphShapingData.cs +++ b/src/SixLabors.Fonts/GlyphShapingData.cs @@ -122,12 +122,12 @@ public GlyphShapingData(GlyphShapingData data, bool clearFeatures = false) /// /// Gets or sets the collection of features. /// - public List Features { get; set; } = new(); + public List Features { get; set; } = []; /// /// Gets or sets the collection of applied features. /// - public HashSet AppliedFeatures { get; set; } = new(); + public HashSet AppliedFeatures { get; set; } = []; /// /// Gets or sets the shaping bounds. diff --git a/src/SixLabors.Fonts/GlyphSubstitutionCollection.cs b/src/SixLabors.Fonts/GlyphSubstitutionCollection.cs index f213b6810..2833bea43 100644 --- a/src/SixLabors.Fonts/GlyphSubstitutionCollection.cs +++ b/src/SixLabors.Fonts/GlyphSubstitutionCollection.cs @@ -202,7 +202,7 @@ public void Clear() /// public bool TryGetGlyphShapingDataAtOffset(int offset, [NotNullWhen(true)] out IReadOnlyList? data) { - List match = new(); + List match = []; for (int i = 0; i < this.glyphs.Count; i++) { if (this.glyphs[i].Offset == offset) @@ -258,7 +258,7 @@ public void Replace(int index, ReadOnlySpan removalIndices, ushort glyphId, CodePoint currentCodePoint = this.glyphs[match].Data.CodePoint; if (!UnicodeUtility.IsDefaultIgnorableCodePoint((uint)codePoint.Value) || UnicodeUtility.ShouldRenderWhiteSpaceOnly(codePoint)) { - if (!CodePoint.IsZeroWidthJoiner(currentCodePoint)) + if (!CodePoint.IsZeroWidthJoiner(currentCodePoint) && !CodePoint.IsZeroWidthNonJoiner(currentCodePoint)) { codePoint = currentCodePoint; } @@ -304,7 +304,7 @@ public void Replace(int index, int count, ushort glyphId, Tag feature) CodePoint currentCodePoint = this.glyphs[match].Data.CodePoint; if (!UnicodeUtility.IsDefaultIgnorableCodePoint((uint)codePoint.Value) || UnicodeUtility.ShouldRenderWhiteSpaceOnly(codePoint)) { - if (!CodePoint.IsZeroWidthJoiner(currentCodePoint)) + if (!CodePoint.IsZeroWidthJoiner(currentCodePoint) && !CodePoint.IsZeroWidthNonJoiner(currentCodePoint)) { codePoint = currentCodePoint; } diff --git a/src/SixLabors.Fonts/GlyphType.cs b/src/SixLabors.Fonts/GlyphType.cs index a5c75c7eb..38dd04176 100644 --- a/src/SixLabors.Fonts/GlyphType.cs +++ b/src/SixLabors.Fonts/GlyphType.cs @@ -19,7 +19,7 @@ public enum GlyphType Standard, /// - /// This is a single layer of the multi-layer colored glyph (emoji). + /// This is a multi-layer colored glyph (emoji). /// - ColrLayer + Painted } diff --git a/src/SixLabors.Fonts/IColorGlyphRenderer.cs b/src/SixLabors.Fonts/IColorGlyphRenderer.cs deleted file mode 100644 index 16f3836d3..000000000 --- a/src/SixLabors.Fonts/IColorGlyphRenderer.cs +++ /dev/null @@ -1,106 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.Fonts; - -/// -/// A surface that can have a glyph rendered to it as a series of actions, where the engine support colored glyphs (emoji). -/// -public interface IColorGlyphRenderer : IGlyphRenderer -{ - /// - /// Sets the color to use for the current glyph. - /// - /// The color to override the renders brush with. - void SetColor(GlyphColor color); -} - -/// -/// Provides access to the color details for the current glyph. -/// -public readonly struct GlyphColor -{ - internal GlyphColor(byte blue, byte green, byte red, byte alpha) - { - this.Blue = blue; - this.Green = green; - this.Red = red; - this.Alpha = alpha; - } - - /// - /// Gets the blue component - /// - public readonly byte Blue { get; } - - /// - /// Gets the green component - /// - public readonly byte Green { get; } - - /// - /// Gets the red component - /// - public readonly byte Red { get; } - - /// - /// Gets the alpha component - /// - public readonly byte Alpha { get; } - - /// - /// Compares two objects for equality. - /// - /// - /// The on the left side of the operand. - /// - /// - /// The on the right side of the operand. - /// - /// - /// True if the current left is equal to the parameter; otherwise, false. - /// - public static bool operator ==(GlyphColor left, GlyphColor right) - => left.Equals(right); - - /// - /// Compares two objects for inequality. - /// - /// - /// The on the left side of the operand. - /// - /// - /// The on the right side of the operand. - /// - /// - /// True if the current left is unequal to the parameter; otherwise, false. - /// - public static bool operator !=(GlyphColor left, GlyphColor right) - => !left.Equals(right); - - /// - public override bool Equals(object? obj) => obj is GlyphColor p && this.Equals(p); - - /// - /// Compares the for equality to this color. - /// - /// - /// The other to compare to. - /// - /// - /// True if the current color is equal to the parameter; otherwise, false. - /// - public bool Equals(GlyphColor other) - => other.Red == this.Red - && other.Green == this.Green - && other.Blue == this.Blue - && other.Alpha == this.Alpha; - - /// - public override int GetHashCode() - => HashCode.Combine( - this.Red, - this.Green, - this.Blue, - this.Alpha); -} diff --git a/src/SixLabors.Fonts/IFontMetricsCollection.cs b/src/SixLabors.Fonts/IFontMetricsCollection.cs index 531a2b032..4225a713a 100644 --- a/src/SixLabors.Fonts/IFontMetricsCollection.cs +++ b/src/SixLabors.Fonts/IFontMetricsCollection.cs @@ -16,11 +16,11 @@ internal interface IFontMetricsCollection : IReadOnlyFontMetricsCollection /// The font metrics to add. /// The culture of the font metrics to add. /// The new . - FontFamily AddMetrics(FontMetrics metrics, CultureInfo culture); + public FontFamily AddMetrics(FontMetrics metrics, CultureInfo culture); /// /// Adds the font metrics to the . /// /// The font metrics to add. - void AddMetrics(FontMetrics metrics); + public void AddMetrics(FontMetrics metrics); } diff --git a/src/SixLabors.Fonts/IGlyphRenderer.cs b/src/SixLabors.Fonts/IGlyphRenderer.cs deleted file mode 100644 index f599b2734..000000000 --- a/src/SixLabors.Fonts/IGlyphRenderer.cs +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; - -namespace SixLabors.Fonts; - -/// -/// A surface that can have a glyph rendered to it as a series of actions. -/// -public interface IGlyphRenderer -{ - /// - /// Begins the figure. - /// - void BeginFigure(); - - /// - /// Sets a new start point to draw lines from. - /// - /// The point. - void MoveTo(Vector2 point); - - /// - /// Draw a quadratic bezier curve connecting the previous point to . - /// - /// The second control point. - /// The point. - void QuadraticBezierTo(Vector2 secondControlPoint, Vector2 point); - - /// - /// Draw a cubic bezier curve connecting the previous point to . - /// - /// The second control point. - /// The third control point. - /// The point. - void CubicBezierTo(Vector2 secondControlPoint, Vector2 thirdControlPoint, Vector2 point); - - /// - /// Draw a straight line connecting the previous point to . - /// - /// The point. - void LineTo(Vector2 point); - - /// - /// Ends the figure. - /// - void EndFigure(); - - /// - /// Ends the glyph. - /// - void EndGlyph(); - - /// - /// Begins the glyph. - /// - /// The bounds the glyph will be rendered at and at what size. - /// - /// The set of parameters that uniquely represents a version of a glyph in at particular font size, font family, font style and DPI. - /// - /// Returns true if the glyph should be rendered otherwise it returns false. - bool BeginGlyph(in FontRectangle bounds, in GlyphRendererParameters parameters); - - /// - /// Called once all glyphs have completed rendering. - /// - void EndText(); - - /// - /// Called before any glyphs have been rendered. - /// - /// The rectangle within the text will be rendered. - void BeginText(in FontRectangle bounds); - - /// - /// Provides a callback to enable custom logic to request decoration details. - /// A custom might use alternative triggers to determine what decorations it needs access to. - /// - /// The text decorations the render wants render info for. - public TextDecorations EnabledDecorations(); - - /// - /// Provides the positions required for drawing text decorations onto the - /// - /// The type of decoration these details correspond to. - /// The start position from where to draw the decorations from. - /// The end position from where to draw the decorations to. - /// The thickness to draw the decoration. - public void SetDecoration(TextDecorations textDecorations, Vector2 start, Vector2 end, float thickness); -} diff --git a/src/SixLabors.Fonts/IGlyphShapingCollection.cs b/src/SixLabors.Fonts/IGlyphShapingCollection.cs index 7e0ee27d0..9a56fd64e 100644 --- a/src/SixLabors.Fonts/IGlyphShapingCollection.cs +++ b/src/SixLabors.Fonts/IGlyphShapingCollection.cs @@ -13,38 +13,38 @@ internal interface IGlyphShapingCollection /// /// Gets the collection count. /// - int Count { get; } + public int Count { get; } /// /// Gets the text options used by this collection. /// - TextOptions TextOptions { get; } + public TextOptions TextOptions { get; } /// /// Gets the glyph shaping data at the specified index. /// /// The zero-based index of the elements to get. /// The . - GlyphShapingData this[int index] { get; } + public GlyphShapingData this[int index] { get; } /// /// Adds the shaping feature to the collection which should be applied to the glyph at a specified index. /// /// The zero-based index of the element. /// The feature to apply. - void AddShapingFeature(int index, TagEntry feature); + public void AddShapingFeature(int index, TagEntry feature); /// /// Enables a previously added shaping feature. /// /// The zero-based index of the element. /// The feature to enable. - void EnableShapingFeature(int index, Tag feature); + public void EnableShapingFeature(int index, Tag feature); /// /// Disables a previously added shaping feature. /// /// The zero-based index of the element. /// The feature to disable. - void DisableShapingFeature(int index, Tag feature); + public void DisableShapingFeature(int index, Tag feature); } diff --git a/src/SixLabors.Fonts/IReadonlyFontCollection.cs b/src/SixLabors.Fonts/IReadonlyFontCollection.cs index 19405b0a7..008ca13e3 100644 --- a/src/SixLabors.Fonts/IReadonlyFontCollection.cs +++ b/src/SixLabors.Fonts/IReadonlyFontCollection.cs @@ -14,7 +14,7 @@ public interface IReadOnlyFontCollection /// Gets the collection of in this /// using the invariant culture. /// - IEnumerable Families { get; } + public IEnumerable Families { get; } /// /// Gets the specified font family matching the invariant culture and font family name. @@ -23,7 +23,7 @@ public interface IReadOnlyFontCollection /// The first matching the given name. /// is /// The collection contains no matches. - FontFamily Get(string name); + public FontFamily Get(string name); /// /// Gets the specified font family matching the invariant culture and font family name. @@ -39,7 +39,7 @@ public interface IReadOnlyFontCollection /// with the specified name; otherwise, . /// /// is - bool TryGet(string name, out FontFamily family); + public bool TryGet(string name, out FontFamily family); /// /// Gets the collection of in this @@ -47,7 +47,7 @@ public interface IReadOnlyFontCollection /// /// The culture of the families to return. /// The . - IEnumerable GetByCulture(CultureInfo culture); + public IEnumerable GetByCulture(CultureInfo culture); /// /// Gets the specified font family matching the given culture and font family name. @@ -57,7 +57,7 @@ public interface IReadOnlyFontCollection /// The first matching the given name. /// is /// The collection contains no matches. - FontFamily Get(string name, CultureInfo culture); + public FontFamily GetByCulture(string name, CultureInfo culture); /// /// Gets the specified font family matching the given culture and font family name. @@ -74,5 +74,5 @@ public interface IReadOnlyFontCollection /// with the specified name; otherwise, . /// /// is - bool TryGet(string name, CultureInfo culture, out FontFamily family); + public bool TryGetByCulture(string name, CultureInfo culture, out FontFamily family); } diff --git a/src/SixLabors.Fonts/IReadonlyFontMetricsCollection.cs b/src/SixLabors.Fonts/IReadonlyFontMetricsCollection.cs index a1e42d7c2..0b11f645f 100644 --- a/src/SixLabors.Fonts/IReadonlyFontMetricsCollection.cs +++ b/src/SixLabors.Fonts/IReadonlyFontMetricsCollection.cs @@ -28,7 +28,7 @@ internal interface IReadOnlyFontMetricsCollection /// with the specified name; otherwise, . /// /// is - bool TryGetMetrics(string name, CultureInfo culture, FontStyle style, [NotNullWhen(true)] out FontMetrics? metrics); + public bool TryGetMetrics(string name, CultureInfo culture, FontStyle style, [NotNullWhen(true)] out FontMetrics? metrics); /// /// Gets the collection of available font metrics for a given culture and font family name. @@ -37,7 +37,7 @@ internal interface IReadOnlyFontMetricsCollection /// The culture to use when searching for a match. /// The . /// is - IEnumerable GetAllMetrics(string name, CultureInfo culture); + public IEnumerable GetAllMetrics(string name, CultureInfo culture); /// /// Gets the collection of available font styles for a given culture and font family name. @@ -46,8 +46,8 @@ internal interface IReadOnlyFontMetricsCollection /// The culture to use when searching for a match. /// The . /// is - IEnumerable GetAllStyles(string name, CultureInfo culture); + public IEnumerable GetAllStyles(string name, CultureInfo culture); /// - IEnumerator GetEnumerator(); + public IEnumerator GetEnumerator(); } diff --git a/src/SixLabors.Fonts/Native/CoreFoundation.cs b/src/SixLabors.Fonts/Native/CoreFoundation.cs index 9df379bc4..2bb3aabef 100644 --- a/src/SixLabors.Fonts/Native/CoreFoundation.cs +++ b/src/SixLabors.Fonts/Native/CoreFoundation.cs @@ -45,7 +45,7 @@ internal static class CoreFoundation /// A value of type CFTypeID that identifies the opaque type of . /// /// This function returns a value that uniquely identifies the opaque type of any Core Foundation object. - /// You can compare this value with the known CFTypeID identifier obtained with a “GetTypeID” function specific to a type, for example CFDateGetTypeID. + /// You can compare this value with the known CFTypeID identifier obtained with a "GetTypeID" function specific to a type, for example CFDateGetTypeID. /// These values might change from release to release or platform to platform. /// [DllImport(CoreFoundationFramework, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] @@ -72,7 +72,7 @@ internal static class CoreFoundation /// The length of in bytes. /// The string encoding to which the character contents of should be converted. The encoding must specify an 8-bit encoding. /// upon success or if the conversion fails or the provided buffer is too small. - /// This function is useful when you need your own copy of a string’s character data as a C string. You also typically call it as a “backup” when a prior call to the function fails. + /// This function is useful when you need your own copy of a string’s character data as a C string. You also typically call it as a "backup" when a prior call to the function fails. [DllImport(CoreFoundationFramework, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] [System.Diagnostics.CodeAnalysis.SuppressMessage("Interoperability", "SYSLIB1054:Use 'LibraryImportAttribute' instead of 'DllImportAttribute' to generate P/Invoke marshalling code at compile time", Justification = ".NET7 Only")] public static extern bool CFStringGetCString(IntPtr theString, byte[] buffer, long bufferSize, CFStringEncoding encoding); diff --git a/src/SixLabors.Fonts/Rendering/CompositeMode.cs b/src/SixLabors.Fonts/Rendering/CompositeMode.cs new file mode 100644 index 000000000..e3225b7da --- /dev/null +++ b/src/SixLabors.Fonts/Rendering/CompositeMode.cs @@ -0,0 +1,183 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Rendering; + +/// +/// Defines compositing and blending operations used when combining source and destination colors. +/// +/// Values 0–12 correspond to standard Porter–Duff compositing modes. These determine how source and +/// destination alpha interact to produce transparency. The remaining values (13–27) correspond to +/// separable and non-separable blend modes used in modern graphics systems such as ImageSharp. +/// +/// +public enum CompositeMode +{ + // --- Porter–Duff compositing modes --- + + /// + /// Clears both the source and destination. + /// The output is fully transparent regardless of the input colors. + /// + Clear = 0, + + /// + /// Replaces the destination entirely with the source. + /// The destination pixels are ignored. + /// + Src = 1, + + /// + /// Keeps the destination as-is and ignores the source. + /// Equivalent to no drawing operation. + /// + Dest = 2, + + /// + /// Draws the source over the destination using standard alpha compositing. + /// The source appears on top and the destination shows through transparent areas. + /// + SrcOver = 3, + + /// + /// Draws the destination over the source. + /// The destination appears on top and the source shows through transparent areas. + /// + DestOver = 4, + + /// + /// Shows the source only where it overlaps the destination. + /// The destination’s alpha acts as a mask for the source. + /// + SrcIn = 5, + + /// + /// Shows the destination only where it overlaps the source. + /// The source’s alpha acts as a mask for the destination. + /// + DestIn = 6, + + /// + /// Shows the source only where it does not overlap the destination. + /// Produces the inverse of . + /// + SrcOut = 7, + + /// + /// Shows the destination only where it does not overlap the source. + /// Produces the inverse of . + /// + DestOut = 8, + + /// + /// Draws the source over the destination but only within the destination’s alpha region. + /// Outside that region, the destination remains unchanged. + /// + SrcAtop = 9, + + /// + /// Draws the destination over the source but only within the source’s alpha region. + /// Outside that region, the source is visible. + /// + DestAtop = 10, + + /// + /// Exclusive OR. + /// Shows the source and destination only where they do not overlap. + /// Overlapping regions become transparent. + /// + Xor = 11, + + /// + /// Adds the source and destination color values. + /// Alpha is also added, producing a brightening effect. + /// + Plus = 12, + + // --- Separable and non-separable blend modes --- + + /// + /// Combines colors using an inverse multiply. + /// Formula: 1 − (1 − S) × (1 − D). + /// Produces a lighter result similar to photographic screen exposure. + /// + Screen = 13, + + /// + /// Multiplies or screens colors depending on destination lightness. + /// Preserves highlights and shadows while mixing source and destination tones. + /// + Overlay = 14, + + /// + /// Chooses the darker of source and destination values per color channel. + /// + Darken = 15, + + /// + /// Chooses the lighter of source and destination values per color channel. + /// + Lighten = 16, + + /// + /// Brightens the destination to reflect the source. + /// Formula: D / (1 − S). + /// + ColorDodge = 17, + + /// + /// Darkens the destination to reflect the source. + /// Formula: 1 − (1 − D) / S. + /// + ColorBurn = 18, + + /// + /// Applies overlay logic using the source’s lightness. + /// Used for strong highlight and shadow effects. + /// + HardLight = 19, + + /// + /// Similar to , but with reduced contrast. + /// Produces a softer transition between tones. + /// + SoftLight = 20, + + /// + /// Subtracts darker colors from lighter ones to highlight differences. + /// Often used for comparison or edge detection effects. + /// + Difference = 21, + + /// + /// Similar to , but with reduced contrast. + /// Midtones are preserved, producing a lower-contrast difference. + /// + Exclusion = 22, + + /// + /// Multiplies source and destination colors. + /// Always results in a darker composite. + /// + Multiply = 23, + + /// + /// Combines the hue of the source with the saturation and luminosity of the destination. + /// + Hue = 24, + + /// + /// Combines the saturation of the source with the hue and luminosity of the destination. + /// + Saturation = 25, + + /// + /// Combines the hue and saturation of the source with the luminosity of the destination. + /// + Color = 26, + + /// + /// Combines the luminosity of the source with the hue and saturation of the destination. + /// + Luminosity = 27 +} diff --git a/src/SixLabors.Fonts/Rendering/FillRule.cs b/src/SixLabors.Fonts/Rendering/FillRule.cs new file mode 100644 index 000000000..128f83e76 --- /dev/null +++ b/src/SixLabors.Fonts/Rendering/FillRule.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Rendering; + +/// +/// Specifies the fill rule for path rasterization. +/// +public enum FillRule +{ + /// + /// Non-zero winding rule. + /// + NonZero = 0, + + /// + /// Even-odd rule. + /// + EvenOdd = 1, +} diff --git a/src/SixLabors.Fonts/IGlyphRendererExtensions.cs b/src/SixLabors.Fonts/Rendering/GlyphRendererExtensions.cs similarity index 89% rename from src/SixLabors.Fonts/IGlyphRendererExtensions.cs rename to src/SixLabors.Fonts/Rendering/GlyphRendererExtensions.cs index d8937b6cd..510bb8390 100644 --- a/src/SixLabors.Fonts/IGlyphRendererExtensions.cs +++ b/src/SixLabors.Fonts/Rendering/GlyphRendererExtensions.cs @@ -1,12 +1,12 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.Fonts; +namespace SixLabors.Fonts.Rendering; /// /// A surface that can have a glyph rendered to it as a series of actions. /// -public static class IGlyphRendererExtensions +public static class GlyphRendererExtensions { /// /// Renders the text. diff --git a/src/SixLabors.Fonts/GlyphRendererParameters.cs b/src/SixLabors.Fonts/Rendering/GlyphRendererParameters.cs similarity index 86% rename from src/SixLabors.Fonts/GlyphRendererParameters.cs rename to src/SixLabors.Fonts/Rendering/GlyphRendererParameters.cs index 3325d8ab6..e55e2f3a0 100644 --- a/src/SixLabors.Fonts/GlyphRendererParameters.cs +++ b/src/SixLabors.Fonts/Rendering/GlyphRendererParameters.cs @@ -5,7 +5,7 @@ using System.Globalization; using SixLabors.Fonts.Unicode; -namespace SixLabors.Fonts; +namespace SixLabors.Fonts.Rendering; /// /// The combined set of properties that uniquely identify the glyph that is to be rendered @@ -19,15 +19,16 @@ internal GlyphRendererParameters( TextRun textRun, float pointSize, float dpi, - GlyphLayoutMode layoutMode) + GlyphLayoutMode layoutMode, + int graphemeIndex) { this.Font = metrics.FontMetrics.Description.FontNameInvariantCulture?.ToUpper(CultureInfo.InvariantCulture) ?? string.Empty; this.FontStyle = metrics.FontMetrics.Description.Style; this.GlyphId = metrics.GlyphId; + this.GraphemeIndex = graphemeIndex; this.PointSize = pointSize; this.Dpi = dpi; this.GlyphType = metrics.GlyphType; - this.GlyphColor = metrics.GlyphColor ?? default; this.TextRun = textRun; this.CodePoint = metrics.CodePoint; this.LayoutMode = layoutMode; @@ -38,11 +39,6 @@ internal GlyphRendererParameters( /// public string Font { get; } - /// - /// Gets the color details of this glyph. - /// - public GlyphColor GlyphColor { get; } - /// /// Gets the type of this glyph. /// @@ -58,6 +54,16 @@ internal GlyphRendererParameters( /// public ushort GlyphId { get; } + /// + /// Gets the id of the composite glyph if the is ; + /// + public ushort CompositeGlyphId { get; } + + /// + /// Gets the index of the grapheme this glyph belongs to. + /// + public int GraphemeIndex { get; } + /// /// Gets the codepoint represented by this glyph. /// @@ -119,11 +125,12 @@ public bool Equals(GlyphRendererParameters other) && other.FontStyle == this.FontStyle && other.Dpi == this.Dpi && other.GlyphId == this.GlyphId + && other.CompositeGlyphId == this.CompositeGlyphId + && this.GraphemeIndex == other.GraphemeIndex && other.GlyphType == this.GlyphType && other.TextRun.TextAttributes == this.TextRun.TextAttributes && other.TextRun.TextDecorations == this.TextRun.TextDecorations && other.LayoutMode == this.LayoutMode - && other.GlyphColor.Equals(this.GlyphColor) && ((other.Font is null && this.Font is null) || (other.Font?.Equals(this.Font, StringComparison.OrdinalIgnoreCase) == true)); @@ -139,15 +146,18 @@ public override int GetHashCode() this.PointSize, this.GlyphId, this.GlyphType, - this.GlyphColor); + this.FontStyle); int b = HashCode.Combine( - this.FontStyle, this.Dpi, this.TextRun.TextAttributes, this.TextRun.TextDecorations, this.LayoutMode); - return HashCode.Combine(a, b); + int c = HashCode.Combine( + this.CompositeGlyphId, + this.GraphemeIndex); + + return HashCode.Combine(a, b, c); } } diff --git a/src/SixLabors.Fonts/Rendering/GradientStop.cs b/src/SixLabors.Fonts/Rendering/GradientStop.cs new file mode 100644 index 000000000..3d9a8c6b7 --- /dev/null +++ b/src/SixLabors.Fonts/Rendering/GradientStop.cs @@ -0,0 +1,33 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Rendering; + +/// +/// Defines a color stop for gradient paints. +/// Offsets must be clamped to the range [0, 1] by the interpreter. +/// Colors are direct RGBA and must not reference palettes. +/// +public readonly struct GradientStop +{ + /// + /// Initializes a new instance of the struct. + /// + /// The stop position in the range [0, 1]. + /// The color at the stop. + public GradientStop(float offset, GlyphColor color) + { + this.Offset = offset; + this.Color = color; + } + + /// + /// Gets the stop position in the range [0, 1]. + /// + public float Offset { get; } + + /// + /// Gets the color at the stop (direct RGBA). + /// + public GlyphColor Color { get; } +} diff --git a/src/SixLabors.Fonts/Rendering/GradientUnits.cs b/src/SixLabors.Fonts/Rendering/GradientUnits.cs new file mode 100644 index 000000000..4a7585233 --- /dev/null +++ b/src/SixLabors.Fonts/Rendering/GradientUnits.cs @@ -0,0 +1,22 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Rendering; + +/// +/// Coordinate system to interpret gradient geometry. +/// +public enum GradientUnits +{ + /// + /// Coordinates are normalized to the painted geometry's bounds ([0, 1] in X and Y). + /// The renderer will map these to the actual path bounds at paint time. + /// + ObjectBoundingBox = 0, + + /// + /// Coordinates are absolute in the same space as the already-transformed geometry. + /// Interpreters must pre-apply any gradient transforms before creating the paint. + /// + UserSpaceOnUse = 1, +} diff --git a/src/SixLabors.Fonts/Rendering/IGlyphRenderer.cs b/src/SixLabors.Fonts/Rendering/IGlyphRenderer.cs new file mode 100644 index 000000000..d24b48c01 --- /dev/null +++ b/src/SixLabors.Fonts/Rendering/IGlyphRenderer.cs @@ -0,0 +1,135 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.Fonts.Rendering; + +/// +/// A surface that can have a glyph rendered to it as a series of actions. +/// +public interface IGlyphRenderer +{ + /// + /// Called before any glyphs have been rendered. + /// + /// The rectangle within the text will be rendered. + public void BeginText(in FontRectangle bounds); + + /// + /// Called once all glyphs have completed rendering. + /// + public void EndText(); + + /// + /// Begins the glyph. + /// + /// The bounds the glyph will be rendered at and at what size. + /// + /// The set of parameters that uniquely represents a version of a glyph at particular font size, font family, font style and DPI. + /// + /// + /// Returns if the glyph should be rendered otherwise it returns . + /// + public bool BeginGlyph(in FontRectangle bounds, in GlyphRendererParameters parameters); + + /// + /// Ends the glyph. + /// + public void EndGlyph(); + + /// + /// Begins a new painted layer with the specified paint and fill rule. + /// All geometry commands issued after this call belong to the layer until is called. + /// + /// The paint definition. + /// The fill rule to use when rasterizing this layer. + /// The optional clip bounds to apply when rasterizing this layer. + public void BeginLayer(Paint? paint, FillRule fillRule, ClipQuad? clipBounds); + + /// + /// Ends the current painted layer. + /// + public void EndLayer(); + + /// + /// Begins the figure. + /// + public void BeginFigure(); + + /// + /// Sets a new start point to draw lines from. + /// + /// The point. + public void MoveTo(Vector2 point); + + /// + /// Draw a straight line connecting the previous point to . + /// + /// The point. + public void LineTo(Vector2 point); + + /// + /// Draw a quadratic bezier curve connecting the previous point to . + /// + /// The second control point. + /// The point. + public void QuadraticBezierTo(Vector2 secondControlPoint, Vector2 point); + + /// + /// Draw a cubic bezier curve connecting the previous point to . + /// + /// The second control point. + /// The third control point. + /// The point. + public void CubicBezierTo(Vector2 secondControlPoint, Vector2 thirdControlPoint, Vector2 point); + + /// + /// + /// Adds an elliptical arc to the current figure. The arc curves from the last point to , + /// choosing one of four possible routes: clockwise or counterclockwise, and smaller or larger. + /// + /// + /// The arc sweep is always less than 360 degrees. The method appends a line + /// to the last point if either radii are zero, or if last point is equal to . + /// In addition the method scales the radii to fit last point and if both + /// are greater than zero but too small to describe an arc. + /// + /// + /// The x-radius of the ellipsis. + /// The y-radius of the ellipsis. + /// The rotation along the X-axis; measured in degrees clockwise. + /// + /// The large arc flag, and is if an arc spanning less than or equal to 180 degrees + /// is chosen, or if an arc spanning greater than 180 degrees is chosen. + /// + /// + /// The sweep flag, and is if the line joining center to arc sweeps through decreasing + /// angles, or if it sweeps through increasing angles. + /// + /// The end point of the arc. + public void ArcTo(float radiusX, float radiusY, float rotation, bool largeArc, bool sweep, Vector2 point); + + /// + /// Ends the figure. + /// + public void EndFigure(); + + /// + /// Provides a callback to enable custom logic to request decoration details. + /// A custom might use alternative triggers to determine what decorations it needs access to. + /// + /// The text decorations the render wants render info for. + public TextDecorations EnabledDecorations(); + + /// + /// Sets the details of a text decoration to be rendered. + /// This only gets called if the decoration type was requested via + /// and after the glyph has been rendered via and . + /// + /// The type of decoration these details correspond to. + /// The start position from where to draw the decorations from. + /// The end position from where to draw the decorations to. + /// The thickness to draw the decoration. + public void SetDecoration(TextDecorations textDecorations, Vector2 start, Vector2 end, float thickness); +} diff --git a/src/SixLabors.Fonts/Rendering/IPaintedGlyphSource.cs b/src/SixLabors.Fonts/Rendering/IPaintedGlyphSource.cs new file mode 100644 index 000000000..cac5e8e34 --- /dev/null +++ b/src/SixLabors.Fonts/Rendering/IPaintedGlyphSource.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Rendering; + +/// +/// Supplies painted glyphs (layers + commands + paints) and canvas metadata for a glyph id. +/// Interpreters (e.g., COLR v1, OT-SVG) implement this interface. +/// +internal interface IPaintedGlyphSource +{ + /// + /// Attempts to get a painted glyph and its canvas metadata. + /// + /// The glyph id. + /// The painted glyph. + /// The canvas metadata. + /// if the glyph is available; otherwise . + public bool TryGetPaintedGlyph(ushort glyphId, out PaintedGlyph glyph, out PaintedCanvasMetadata canvas); +} diff --git a/src/SixLabors.Fonts/Rendering/Paint.cs b/src/SixLabors.Fonts/Rendering/Paint.cs new file mode 100644 index 000000000..04ee4792f --- /dev/null +++ b/src/SixLabors.Fonts/Rendering/Paint.cs @@ -0,0 +1,165 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.Fonts.Rendering; + +/// +/// Base type for normalized paint definitions that can be used by any renderer. +/// Glyph sources must pre-apply all relevant transforms and resolve any palette +/// or format-specific constructs before creating a paint instance. +/// +public abstract class Paint +{ + /// + /// Gets the per-layer opacity multiplier in the range [0, 1]. + /// Renderers should multiply this value into the alpha channel of the final brush. + /// + public float Opacity { get; init; } = 1f; + + /// + /// Gets or sets an optional transform to apply to the paint. + /// Used to pre-apply gradientTransform in SVG or equivalent. + /// + internal Matrix3x2 Transform { get; set; } + + /// + /// Gets the composite mode to use when applying this paint over existing content. + /// + public CompositeMode CompositeMode { get; init; } = CompositeMode.SrcOver; +} + +/// +/// Solid color paint (direct RGBA). Interpreters must resolve palettes to RGBA. +/// Compatible with OT-SVG solid fills and COLR v1 PaintSolid after CPAL resolution. +/// +public sealed class SolidPaint : Paint +{ + /// + /// Gets the color to use for the fill. Alpha is respected and further multiplied by . + /// + public GlyphColor Color { get; init; } +} + +/// +/// Linear gradient paint. +/// +public sealed class LinearGradientPaint : Paint +{ + /// + /// Gets the coordinate system for and . + /// + internal GradientUnits Units { get; init; } + + /// + /// Gets the gradient start point. Normalized if is . + /// + public Vector2 P0 { get; init; } + + /// + /// Gets the gradient end point. Normalized if is . + /// + public Vector2 P1 { get; init; } + + /// + /// Gets the rotation point for the gradient. Normalized if is . + /// + public Vector2? P2 { get; init; } + + /// + /// Gets the spread method applied when sampling outside the [0, 1] range. + /// + public SpreadMethod Spread { get; init; } = SpreadMethod.Pad; + + /// + /// Gets the ordered gradient stops (ascending by ). + /// + public GradientStop[] Stops { get; init; } = []; +} + +/// +/// Represents a radial gradient paint defined by two circles. +/// The first circle is centered at with radius . +/// The second circle is centered at with radius . +/// The color transition is computed between these two circles. +/// Compatible with two-circle radial gradients used by HTML Canvas and OpenType COLR v1. +/// +public sealed class RadialGradientPaint : Paint +{ + /// + /// Gets the coordinate system for , , + /// , and . + /// + internal GradientUnits Units { get; init; } + + /// + /// Gets the center of the starting circle of the gradient. + /// + public Vector2 Center0 { get; init; } + + /// + /// Gets the radius of the starting circle of the gradient. + /// If is , + /// the radius is normalized to the bounds. + /// + public float Radius0 { get; init; } + + /// + /// Gets the center of the ending circle of the gradient. + /// + public Vector2 Center1 { get; init; } + + /// + /// Gets the radius of the ending circle of the gradient. + /// If is , + /// the radius is normalized to the bounds. + /// + public float Radius1 { get; init; } + + /// + /// Gets the spread method applied when sampling outside the [0, 1] range. + /// + public SpreadMethod Spread { get; init; } = SpreadMethod.Pad; + + /// + /// Gets the ordered gradient stops, ascending by . + /// + public GradientStop[] Stops { get; init; } = []; +} + +/// +/// Sweep (conic) gradient paint. Angles are expressed in degrees in the renderer's y-down space. +/// +public sealed class SweepGradientPaint : Paint +{ + /// + /// Gets the coordinate system for . Sweep gradients are typically user-space. + /// + internal GradientUnits Units { get; init; } = GradientUnits.UserSpaceOnUse; + + /// + /// Gets the center of the sweep gradient. + /// + public Vector2 Center { get; init; } + + /// + /// Gets the start angle in degrees. + /// + public float StartAngle { get; init; } + + /// + /// Gets the end angle in degrees. + /// + public float EndAngle { get; init; } + + /// + /// Gets the spread method applied when sampling outside the [0, 1] range. + /// + public SpreadMethod Spread { get; init; } = SpreadMethod.Pad; + + /// + /// Gets the ordered gradient stops (ascending by ). + /// + public GradientStop[] Stops { get; init; } = []; +} diff --git a/src/SixLabors.Fonts/Rendering/PaintedCanvasMetadata.cs b/src/SixLabors.Fonts/Rendering/PaintedCanvasMetadata.cs new file mode 100644 index 000000000..186858407 --- /dev/null +++ b/src/SixLabors.Fonts/Rendering/PaintedCanvasMetadata.cs @@ -0,0 +1,46 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.Fonts.Rendering; + +/// +/// Canvas metadata describing the document-space coordinate system for a painted glyph. +/// +internal readonly struct PaintedCanvasMetadata +{ + /// + /// Initializes a new instance of the struct. + /// + /// The viewBox rectangle (minX, minY, width, height). + /// True if the source coordinate system is y-down; false if y-up. + /// An optional root transform in document-space. + public PaintedCanvasMetadata(FontRectangle viewBox, bool isYDown, Matrix3x2 rootTransform) + { + this.HasViewBox = viewBox != FontRectangle.Empty; + this.ViewBox = viewBox; + this.IsYDown = isYDown; + this.RootTransform = rootTransform; + } + + /// + /// Gets a value indicating whether a root viewBox is present. + /// + public bool HasViewBox { get; } + + /// + /// Gets the viewBox. + /// + public FontRectangle ViewBox { get; } + + /// + /// Gets a value indicating whether the source coordinate system is y-down. + /// + public bool IsYDown { get; } + + /// + /// Gets the root transform in document-space. + /// + public Matrix3x2 RootTransform { get; } +} diff --git a/src/SixLabors.Fonts/Rendering/PaintedGlyph.cs b/src/SixLabors.Fonts/Rendering/PaintedGlyph.cs new file mode 100644 index 000000000..30222b685 --- /dev/null +++ b/src/SixLabors.Fonts/Rendering/PaintedGlyph.cs @@ -0,0 +1,31 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Rendering; + +/// +/// A glyph fully decomposed into painted layers ready for rendering. +/// +internal readonly struct PaintedGlyph +{ + /// + /// Initializes a new instance of the struct. + /// + /// The painted layers. + public PaintedGlyph(List layers) => this.Layers = layers; + + /// + /// Gets the layers for this glyph. + /// + public IReadOnlyList Layers { get; } + + /// + /// Gets a value indicating whether this glyph has no layers. + /// + public bool IsEmpty => this.Layers.Count == 0; + + /// + /// Gets an empty glyph instance. + /// + public static PaintedGlyph Empty => new([]); +} diff --git a/src/SixLabors.Fonts/Rendering/PaintedGlyphMetrics.cs b/src/SixLabors.Fonts/Rendering/PaintedGlyphMetrics.cs new file mode 100644 index 000000000..8bc7d3434 --- /dev/null +++ b/src/SixLabors.Fonts/Rendering/PaintedGlyphMetrics.cs @@ -0,0 +1,580 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.Fonts.Unicode; + +namespace SixLabors.Fonts.Rendering; + +/// +/// Provides painted (layered) glyph rendering for color formats such as COLR v1 and OT-SVG. +/// Geometry and paints are supplied in document-space by an interpreter; all layout transforms +/// (UPEM mapping, DPI/point-size scaling, rotation, final placement) are applied here. +/// +public sealed class PaintedGlyphMetrics : GlyphMetrics +{ + private readonly IPaintedGlyphSource source; + + /// + /// Initializes a new instance of the class. + /// + /// The font metrics. + /// The glyph identifier. + /// The code point. + /// The painted glyph source. + /// The design-space bounds for the glyph. + /// The advance width. + /// The advance height. + /// The left side bearing. + /// The top side bearing. + /// Units per EM. + /// Text attributes. + /// Text decorations. + internal PaintedGlyphMetrics( + StreamFontMetrics font, + ushort glyphId, + CodePoint codePoint, + IPaintedGlyphSource source, + Bounds bounds, + ushort advanceWidth, + ushort advanceHeight, + short leftSideBearing, + short topSideBearing, + ushort unitsPerEM, + TextAttributes textAttributes, + TextDecorations textDecorations) + : base( + font, + glyphId, + codePoint, + bounds, + advanceWidth, + advanceHeight, + leftSideBearing, + topSideBearing, + unitsPerEM, + textAttributes, + textDecorations, + GlyphType.Painted) + => this.source = source; + + /// + /// Initializes a new instance of the class for rendering with overrides. + /// + internal PaintedGlyphMetrics( + StreamFontMetrics font, + ushort glyphId, + CodePoint codePoint, + IPaintedGlyphSource source, + Bounds bounds, + ushort advanceWidth, + ushort advanceHeight, + short leftSideBearing, + short topSideBearing, + ushort unitsPerEM, + Vector2 offset, + Vector2 scaleFactor, + TextRun textRun) + : base( + font, + glyphId, + codePoint, + bounds, + advanceWidth, + advanceHeight, + leftSideBearing, + topSideBearing, + unitsPerEM, + offset, + scaleFactor, + textRun, + GlyphType.Painted) + => this.source = source; + + /// + internal override GlyphMetrics CloneForRendering(TextRun textRun) + => new PaintedGlyphMetrics( + this.FontMetrics, + this.GlyphId, + this.CodePoint, + this.source, + this.Bounds, + this.AdvanceWidth, + this.AdvanceHeight, + this.LeftSideBearing, + this.TopSideBearing, + this.UnitsPerEm, + this.Offset, + this.ScaleFactor, + textRun); + + /// + internal override void RenderTo( + IGlyphRenderer renderer, + int graphemeIndex, + Vector2 location, + Vector2 offset, + GlyphLayoutMode mode, + TextOptions options) + { + if (ShouldSkipGlyphRendering(this.CodePoint)) + { + return; + } + + float pointSize = this.TextRun.Font?.Size ?? options.Font.Size; + float dpi = options.Dpi; + + // Device-space placement. + location *= dpi; + offset *= dpi; + Vector2 renderLocation = location + offset; + + float scaledPpem = this.GetScaledSize(pointSize, dpi); + Vector2 scale = new Vector2(scaledPpem) / this.ScaleFactor; // uniform + + Matrix3x2 rotation = GetRotationMatrix(mode); + + // Layout similarity: uniform scale then rotation; translation added below. + Matrix3x2 layout = Matrix3x2.CreateScale(scale); + layout *= rotation; + layout.Translation = (this.Offset * scale) + renderLocation; + + // Bounds in device space for BeginGlyph. + FontRectangle box = this.GetBoundingBox(mode, renderLocation, scaledPpem); + GlyphRendererParameters parameters = new(this, this.TextRun, pointSize, dpi, mode, graphemeIndex); + + if (renderer.BeginGlyph(in box, in parameters)) + { + if (!UnicodeUtility.ShouldRenderWhiteSpaceOnly(this.CodePoint) + && this.source.TryGetPaintedGlyph(this.GlyphId, out PaintedGlyph glyph, out PaintedCanvasMetadata canvas)) + { + // Source-to-UPEM: viewBox mapping (uniform "meet"), optional y-flip, optional root transform. + Matrix3x2 s2u = ComputeSourceToUpem(canvas, this.UnitsPerEm); + + // Full transform from source doc-space to device space. + Matrix3x2 total = s2u * layout; + + // Stream layers and commands with correct transforms. + StreamPaintedGlyph(glyph, in box, renderer, total); + } + + renderer.EndGlyph(); + this.RenderDecorationsTo(renderer, location, mode, rotation, scaledPpem, options); + } + } + + /// + /// Computes the mapping from the interpreter's document-space to UPEM font space. + /// Enforces a uniform 'meet' scale from the root viewBox (if present) and flips Y + /// only if the source is y-up. + /// + private static Matrix3x2 ComputeSourceToUpem(in PaintedCanvasMetadata canvas, ushort upem) + { + Matrix3x2 m = Matrix3x2.Identity; + + // Root transform (doc-space). Apply first if provided. + if (!canvas.RootTransform.IsIdentity) + { + m *= canvas.RootTransform; + } + + // Translate viewBox min to origin, then uniform scale to UPEM using "meet". + if (canvas.HasViewBox) + { + Matrix3x2 t = Matrix3x2.CreateTranslation(-canvas.ViewBox.X, -canvas.ViewBox.Y); + + float sx = upem / Math.Max(canvas.ViewBox.Width, 1e-6f); + float sy = upem / Math.Max(canvas.ViewBox.Height, 1e-6f); + float s = MathF.Min(sx, sy); + + Matrix3x2 sUni = Matrix3x2.CreateScale(s); + + m = m * t * sUni; + } + + // Coordinate system orientation. + if (!canvas.IsYDown) + { + // Flip Y around the origin; placement happens in layout. + m *= Matrix3x2.CreateScale(1f, -1f); + } + + return m; + } + + /// + /// Streams the painted glyph to the renderer, transforming geometry and userSpaceOnUse paints. + /// + /// The painted glyph. + /// The device-space bounds of the glyph. + /// The glyph renderer. + /// The full device-space transform to apply. + private static void StreamPaintedGlyph( + in PaintedGlyph glyph, + in FontRectangle bounds, + IGlyphRenderer renderer, + Matrix3x2 xform) + { + IReadOnlyList layers = glyph.Layers; + for (int i = 0; i < layers.Count; i++) + { + PaintedLayer layer = layers[i]; + + // pre-applied transforms (element/group) + Matrix3x2 layerXform = layer.Transform * xform; + + // Clip bounds in device space (if any). + ClipQuad? clipBounds = layer.ClipBounds.HasValue + ? ClipQuad.FromBounds(layer.ClipBounds.Value, layerXform) + : null; + + // Similarity decomposition for arc radii/angle/sweep adjustment (from layer). + Similarity sim = Similarity.FromMatrix(layerXform); + + // Transform userSpaceOnUse paints into device space; keep ObjectBoundingBox normalized. + Paint? paint = TransformPaint(layer.Paint, in bounds, layerXform); + + renderer.BeginLayer(paint, layer.FillRule, clipBounds); + + bool open = false; + IReadOnlyList cmds = layer.Path; + + for (int j = 0; j < cmds.Count; j++) + { + PathCommand c = cmds[j]; + switch (c.Verb) + { + case PathVerb.MoveTo: + { + if (!open) + { + renderer.BeginFigure(); + open = true; + } + + renderer.MoveTo(Vector2.Transform(c.EndPoint, layerXform)); + break; + } + + case PathVerb.LineTo: + { + renderer.LineTo(Vector2.Transform(c.EndPoint, layerXform)); + break; + } + + case PathVerb.QuadraticTo: + { + renderer.QuadraticBezierTo( + Vector2.Transform(c.ControlPoint1, layerXform), + Vector2.Transform(c.EndPoint, layerXform)); + break; + } + + case PathVerb.CubicTo: + { + renderer.CubicBezierTo( + Vector2.Transform(c.ControlPoint1, layerXform), + Vector2.Transform(c.ControlPoint2, layerXform), + Vector2.Transform(c.EndPoint, layerXform)); + break; + } + + case PathVerb.ArcTo: + { + // Adjust radii by the scale component of the transform; + // angle/sweep by the similarity component; + // endpoint is fully transformed. + float rx = c.RadiusX * layerXform.M11; + float ry = c.RadiusY * layerXform.M12; + float ang = c.RotationDegrees + sim.RotationDegrees; + bool sweep = sim.Reflection ? !c.Sweep : c.Sweep; + + renderer.ArcTo(rx, ry, ang, c.LargeArc, sweep, Vector2.Transform(c.EndPoint, layerXform)); + break; + } + + case PathVerb.ClosePath: + { + if (open) + { + renderer.EndFigure(); + open = false; + } + + break; + } + } + } + + if (open) + { + renderer.EndFigure(); + } + + renderer.EndLayer(); + } + } + + /// + /// Converts a into device-space geometry for the target layer, + /// removing (baking in) any paint-local transforms. Geometry path commands have already + /// been transformed elsewhere; this method only resolves paint geometry (start/end points, + /// centers, radii, angles) into device space so the renderer can construct brushes directly. + /// + /// Rules: + /// + /// UserSpaceOnUse: Apply in user space, then apply + /// to obtain device-space positions. Emit device-space values. + /// ObjectBoundingBox: Apply in normalized [0..1] box space, + /// then denormalize to device space using . Emit device-space values. + /// Color stops (ratios) remain normalized in [0..1] and are passed through unchanged. + /// All returned paints have identity and are suitable for direct + /// consumption by Drawing brushes (e.g. LinearGradientBrush expects device-space points). + /// + /// + /// + /// The source paint, or . + /// The device-space axis-aligned bounding box of the current layer’s geometry. + /// + /// The full device-space transform applied to this layer’s geometry (e.g., layer * s2u * layout). + /// Used to push UserSpaceOnUse paints into device space. ObjectBoundingBox paints are denormalized + /// using instead. + /// + /// + /// A paint expressed in device-space with identity transform, or + /// if the input was . + /// + private static Paint? TransformPaint( + Paint? paint, + in FontRectangle layerBounds, + Matrix3x2 layerXform) + { + if (paint is null) + { + return null; + } + + switch (paint) + { + case SolidPaint s: + { + return s; + } + + case LinearGradientPaint lg: + { + Vector2 p0; + Vector2 p1; + Vector2? p2; + + if (lg.Units == GradientUnits.UserSpaceOnUse) + { + // USOU: transform directly to device space. + Matrix3x2 paintXForm = lg.Transform * layerXform; + p0 = Vector2.Transform(lg.P0, paintXForm); + p1 = Vector2.Transform(lg.P1, paintXForm); + p2 = lg.P2.HasValue ? Vector2.Transform(lg.P2.Value, paintXForm) : null; + } + else + { + // OBB: transform in normalized [0..1] space, then denormalize to device via layer bounds. + Vector2 n0 = Vector2.Transform(lg.P0, lg.Transform); + Vector2 n1 = Vector2.Transform(lg.P1, lg.Transform); + Vector2? n2 = lg.P2.HasValue ? Vector2.Transform(lg.P2.Value, lg.Transform) : null; + + p0 = Vector2.Transform(DenormalizePoint(n0, layerBounds), layerXform); + p1 = Vector2.Transform(DenormalizePoint(n1, layerBounds), layerXform); + p2 = n2.HasValue ? Vector2.Transform(DenormalizePoint(n2.Value, layerBounds), layerXform) : null; + } + + return new LinearGradientPaint + { + Units = GradientUnits.UserSpaceOnUse, + P0 = p0, + P1 = p1, + P2 = p2, + Spread = lg.Spread, + Stops = lg.Stops, + Opacity = lg.Opacity, + Transform = Matrix3x2.Identity + }; + } + + case RadialGradientPaint rg: + { + Vector2 c0; + Vector2 c1; + float r0; + float r1; + + if (rg.Units == GradientUnits.UserSpaceOnUse) + { + // USOU: transform directly to device space. + Matrix3x2 paintXForm = rg.Transform * layerXform; + + // Centers get full layer transform. + c0 = Vector2.Transform(rg.Center0, paintXForm); + c1 = Vector2.Transform(rg.Center1, paintXForm); + + // Radii scale by uniform similarity only. + Similarity compSim = Similarity.FromMatrix(paintXForm); + r0 = rg.Radius0 * compSim.Scale; + r1 = rg.Radius1 * compSim.Scale; + } + else + { + // OBB: transform in normalized [0..1] space, then denormalize to device via layer bounds. + Vector2 nc0 = Vector2.Transform(rg.Center0, rg.Transform); + Vector2 nc1 = Vector2.Transform(rg.Center1, rg.Transform); + + c0 = Vector2.Transform(DenormalizePoint(nc0, layerBounds), layerXform); + c1 = Vector2.Transform(DenormalizePoint(nc1, layerBounds), layerXform); + + // Radii scale by total similarity (paint * layer). + Matrix3x2 paintXForm = rg.Transform * layerXform; + Similarity compSim = Similarity.FromMatrix(paintXForm); + r0 = rg.Radius0 * compSim.Scale; + r1 = rg.Radius1 * compSim.Scale; + } + + return new RadialGradientPaint + { + Units = GradientUnits.UserSpaceOnUse, + Center0 = c0, + Radius0 = r0, + Center1 = c1, + Radius1 = r1, + Spread = rg.Spread, + Stops = rg.Stops, + Opacity = rg.Opacity, + Transform = Matrix3x2.Identity + }; + } + + case SweepGradientPaint sg: + { + Vector2 center; + float start = sg.StartAngle; + float end = sg.EndAngle; + + if (sg.Units == GradientUnits.UserSpaceOnUse) + { + // USOU: transform directly to device space. + Matrix3x2 paintXForm = sg.Transform * layerXform; + + // Center gets full layer transform. + center = Vector2.Transform(sg.Center, paintXForm); + + // Angles adjust by similarity rotation and reflection only. + Similarity compSim = Similarity.FromMatrix(paintXForm); + start += compSim.RotationDegrees; + end += compSim.RotationDegrees; + if (compSim.Reflection) + { + (start, end) = (end, start); + } + } + else + { + // OBB: transform in normalized [0..1] space, then denormalize to device via layer bounds. + Vector2 nc = Vector2.Transform(sg.Center, sg.Transform); + center = Vector2.Transform(DenormalizePoint(nc, layerBounds), layerXform); + + // Angles adjust by total similarity (paint * layer). + Matrix3x2 paintXForm = sg.Transform * layerXform; + Similarity compSim = Similarity.FromMatrix(paintXForm); + start += compSim.RotationDegrees; + end += compSim.RotationDegrees; + if (compSim.Reflection) + { + (start, end) = (end, start); + } + } + + return new SweepGradientPaint + { + Units = GradientUnits.UserSpaceOnUse, + Center = center, + StartAngle = start, + EndAngle = end, + Spread = sg.Spread, + Stops = sg.Stops, + Opacity = sg.Opacity, + Transform = Matrix3x2.Identity + }; + } + + default: + { + return paint; + } + } + + static Vector2 DenormalizePoint(Vector2 p, in FontRectangle bounds) + => new(bounds.X + (p.X * bounds.Width), bounds.Y + (p.Y * bounds.Height)); + } + + /// + /// Represents the similarity component of a 2D affine transformation. + /// + /// + /// A similarity transformation is an affine transform that preserves an object's shape and angles, + /// allowing only uniform scaling, rotation, and optional reflection. This structure isolates those + /// properties from a general so that dependent operations such as arc or + /// gradient adjustment can apply proportional transformations correctly. + /// + private readonly struct Similarity + { + private Similarity(float scale, float rotationDeg, bool reflection, bool isSimilarity) + { + this.Scale = scale; + this.RotationDegrees = rotationDeg; + this.Reflection = reflection; + this.IsSimilarity = isSimilarity; + } + + /// + /// Gets the length of the first column. + /// + public float Scale { get; } + + /// + /// Gets the rotation in degrees. + /// + public float RotationDegrees { get; } + + /// + /// Gets a value indicating whether this matrix includes a reflection. + public bool Reflection { get; } + + /// + /// Gets a value indicating whether this matrix is a similarity transform. + /// True if columns are orthogonal and equal length within tolerance. + /// + public bool IsSimilarity { get; } + + public static Similarity FromMatrix(in Matrix3x2 m) + { + float a = m.M11, b = m.M12, c = m.M21, d = m.M22; + + // scale = |X column| + float sx = MathF.Sqrt((a * a) + (b * b)); + + // rotation from X column + float rotDeg = MathF.Atan2(b, a) * (180f / MathF.PI); + + // reflection from determinant + bool refl = ((a * d) - (b * c)) < 0f; + + // similarity test: columns orthogonal and same length + float dot = (a * c) + (b * d); + float sy = MathF.Sqrt((c * c) + (d * d)); + const float eps = 1e-4f; + bool ortho = MathF.Abs(dot) <= eps; + bool equal = MathF.Abs(sx - sy) <= eps; + + return new Similarity(sx, rotDeg, refl, ortho && equal && sx > 0f); + } + } +} diff --git a/src/SixLabors.Fonts/Rendering/PaintedLayer.cs b/src/SixLabors.Fonts/Rendering/PaintedLayer.cs new file mode 100644 index 000000000..05ad6a25f --- /dev/null +++ b/src/SixLabors.Fonts/Rendering/PaintedLayer.cs @@ -0,0 +1,57 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.Fonts.Rendering; + +/// +/// A single painted layer comprising a paint, a fill rule, and a path stream. +/// All coordinates must be pre-transformed in Fonts prior to construction. +/// +internal readonly struct PaintedLayer +{ + /// + /// Initializes a new instance of the struct. + /// + /// The paint definition. + /// The fill rule. + /// The transform applied to all path coordinates. + /// An optional clip bounds to apply when rasterizing this layer. + /// The path command stream for this layer. + public PaintedLayer( + Paint? paint, + FillRule fillRule, + Matrix3x2 transform, + Bounds? clipBounds, + IReadOnlyList path) + { + this.Paint = paint; + this.FillRule = fillRule; + this.Transform = transform; + this.ClipBounds = clipBounds; + this.Path = path; + } + + /// + /// Gets the paint definition for this layer. + /// + public Paint? Paint { get; } + + /// + /// Gets the fill rule for rasterization. + /// + public FillRule FillRule { get; } + + /// + /// Gets the transform applied to all path coordinates. + /// + public Matrix3x2 Transform { get; } + + public Bounds? ClipBounds { get; } + + /// + /// Gets the path stream for this layer. + /// + public IReadOnlyList Path { get; } +} diff --git a/src/SixLabors.Fonts/Rendering/PathCommand.cs b/src/SixLabors.Fonts/Rendering/PathCommand.cs new file mode 100644 index 000000000..ed30c6a21 --- /dev/null +++ b/src/SixLabors.Fonts/Rendering/PathCommand.cs @@ -0,0 +1,151 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.Fonts.Rendering; + +/// +/// A single path command with all coordinates already transformed into final render space (y-down). +/// For arc commands, radii and flags must be pre-adjusted by the interpreter. +/// +internal readonly struct PathCommand +{ + /// + /// Initializes a new instance of the struct. + /// + /// The command verb. + /// The end point for the command. + /// The first control point, if used by the verb. + /// The second control point, if used by the verb. + /// The x-radius for . + /// The y-radius for . + /// The x-axis rotation in degrees for . + /// The large-arc flag for . + /// The sweep flag for . + public PathCommand( + PathVerb verb, + Vector2 endPoint, + Vector2 controlPoint1, + Vector2 controlPoint2, + float radiusX, + float radiusY, + float rotationDegrees, + bool largeArc, + bool sweep) + { + this.Verb = verb; + this.EndPoint = endPoint; + this.ControlPoint1 = controlPoint1; + this.ControlPoint2 = controlPoint2; + this.RadiusX = radiusX; + this.RadiusY = radiusY; + this.RotationDegrees = rotationDegrees; + this.LargeArc = largeArc; + this.Sweep = sweep; + } + + /// + /// Gets the command verb. + /// + public PathVerb Verb { get; } + + /// + /// Gets the end point for the command. + /// For and this is the target point. + /// For curves and arcs it is the end point of the segment. + /// + public Vector2 EndPoint { get; } + + /// + /// Gets the first control point (quadratic control or cubic control 1). + /// Not used for , , or . + /// + public Vector2 ControlPoint1 { get; } + + /// + /// Gets the second control point (cubic control 2). + /// Only used for . + /// + public Vector2 ControlPoint2 { get; } + + /// + /// Gets the x-radius for . + /// + public float RadiusX { get; } + + /// + /// Gets the y-radius for . + /// + public float RadiusY { get; } + + /// + /// Gets the rotation of the arc's x-axis in degrees for . + /// + public float RotationDegrees { get; } + + /// + /// Gets a value indicating whether the large-arc flag is set for . + /// + public bool LargeArc { get; } + + /// + /// Gets a value indicating whether the sweep flag is set for . + /// + public bool Sweep { get; } + + /// + /// Creates a command. + /// + /// The destination point. + /// The command. + public static PathCommand MoveTo(Vector2 point) + => new(PathVerb.MoveTo, point, Vector2.Zero, Vector2.Zero, 0f, 0f, 0f, false, false); + + /// + /// Creates a command. + /// + /// The destination point. + /// The command. + public static PathCommand LineTo(Vector2 point) + => new(PathVerb.LineTo, point, Vector2.Zero, Vector2.Zero, 0f, 0f, 0f, false, false); + + /// + /// Creates a command. + /// + /// The control point. + /// The end point. + /// The command. + public static PathCommand QuadraticTo(Vector2 control, Vector2 end) + => new(PathVerb.QuadraticTo, end, control, Vector2.Zero, 0f, 0f, 0f, false, false); + + /// + /// Creates a command. + /// + /// The first control point. + /// The second control point. + /// The end point. + /// The command. + public static PathCommand CubicTo(Vector2 control1, Vector2 control2, Vector2 end) + => new(PathVerb.CubicTo, end, control1, control2, 0f, 0f, 0f, false, false); + + /// + /// Creates an command. + /// + /// The x-radius of the ellipse. + /// The y-radius of the ellipse. + /// The rotation of the ellipse's x-axis in degrees. + /// The large-arc flag. + /// The sweep flag. + /// The end point. + /// The command. + public static PathCommand ArcTo(float radiusX, float radiusY, float rotationDegrees, bool largeArc, bool sweep, Vector2 end) + => new(PathVerb.ArcTo, end, Vector2.Zero, Vector2.Zero, radiusX, radiusY, rotationDegrees, largeArc, sweep); + + /// + /// Creates a command. + /// + /// The command. + public static PathCommand Close() + => new(PathVerb.ClosePath, Vector2.Zero, Vector2.Zero, Vector2.Zero, 0f, 0f, 0f, false, false); +} diff --git a/src/SixLabors.Fonts/Rendering/PathVerb.cs b/src/SixLabors.Fonts/Rendering/PathVerb.cs new file mode 100644 index 000000000..f7a3cbc04 --- /dev/null +++ b/src/SixLabors.Fonts/Rendering/PathVerb.cs @@ -0,0 +1,40 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Rendering; + +/// +/// Path verb identifying the command type. +/// +internal enum PathVerb : byte +{ + /// + /// Moves the current point without drawing. + /// + MoveTo = 0, + + /// + /// Draws a straight line from the current point to the end point. + /// + LineTo = 1, + + /// + /// Draws a quadratic Bézier from the current point to the end point using a single control point. + /// + QuadraticTo = 2, + + /// + /// Draws a cubic Bézier from the current point to the end point using two control points. + /// + CubicTo = 3, + + /// + /// Draws an elliptical arc from the current point to the end point. + /// + ArcTo = 4, + + /// + /// Closes the current subpath. + /// + ClosePath = 5, +} diff --git a/src/SixLabors.Fonts/Rendering/SpreadMethod.cs b/src/SixLabors.Fonts/Rendering/SpreadMethod.cs new file mode 100644 index 000000000..773fe0c60 --- /dev/null +++ b/src/SixLabors.Fonts/Rendering/SpreadMethod.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Rendering; + +/// +/// Specifies how a gradient should extend beyond the [0, 1] range. +/// +public enum SpreadMethod +{ + /// + /// Clamp to the end colors (pad). + /// + Pad = 0, + + /// + /// Mirror the gradient (reflect). + /// + Reflect = 1, + + /// + /// Repeat the gradient (tile). + /// + Repeat = 2, +} diff --git a/src/SixLabors.Fonts/TextRenderer.cs b/src/SixLabors.Fonts/Rendering/TextRenderer.cs similarity index 94% rename from src/SixLabors.Fonts/TextRenderer.cs rename to src/SixLabors.Fonts/Rendering/TextRenderer.cs index e34ede047..2b78466c3 100644 --- a/src/SixLabors.Fonts/TextRenderer.cs +++ b/src/SixLabors.Fonts/Rendering/TextRenderer.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.Fonts; +namespace SixLabors.Fonts.Rendering; /// /// Encapsulated logic for laying out and then rendering text to a surface. @@ -56,7 +56,7 @@ public void RenderText(ReadOnlySpan text, TextOptions options) foreach (GlyphLayout g in glyphsToRender) { - g.Glyph.RenderTo(this.renderer, g.PenLocation, g.Offset, g.LayoutMode, options); + g.Glyph.RenderTo(this.renderer, g.GraphemeIndex, g.PenLocation, g.Offset, g.LayoutMode, options); } this.renderer.EndText(); diff --git a/src/SixLabors.Fonts/SixLabors.Fonts.csproj b/src/SixLabors.Fonts/SixLabors.Fonts.csproj index e3d65840c..6b435d175 100644 --- a/src/SixLabors.Fonts/SixLabors.Fonts.csproj +++ b/src/SixLabors.Fonts/SixLabors.Fonts.csproj @@ -1,4 +1,4 @@ - + SixLabors.Fonts diff --git a/src/SixLabors.Fonts/StreamFontMetrics.Cff.cs b/src/SixLabors.Fonts/StreamFontMetrics.Cff.cs index 29ed9c062..7348a2e6f 100644 --- a/src/SixLabors.Fonts/StreamFontMetrics.Cff.cs +++ b/src/SixLabors.Fonts/StreamFontMetrics.Cff.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.Fonts.Rendering; using SixLabors.Fonts.Tables.AdvancedTypographic; using SixLabors.Fonts.Tables.Cff; using SixLabors.Fonts.Tables.General; @@ -8,6 +9,7 @@ using SixLabors.Fonts.Tables.General.Kern; using SixLabors.Fonts.Tables.General.Name; using SixLabors.Fonts.Tables.General.Post; +using SixLabors.Fonts.Tables.General.Svg; using SixLabors.Fonts.Unicode; namespace SixLabors.Fonts; @@ -50,6 +52,8 @@ private static StreamFontMetrics LoadCompactFont(FontReader reader) ColrTable? colr = reader.TryGetTable(); CpalTable? cpal = reader.TryGetTable(); + SvgTable? svg = reader.TryGetTable(); + CompactFontTables tables = new(cmap, head, hhea, htmx, maxp, name, os2, post, cff!) { Kern = kern, @@ -60,20 +64,23 @@ private static StreamFontMetrics LoadCompactFont(FontReader reader) GPos = gPos, Colr = colr, Cpal = cpal, + Svg = svg }; return new StreamFontMetrics(tables); } - private CffGlyphMetrics CreateCffGlyphMetrics( + private GlyphMetrics CreateCffGlyphMetrics( in CodePoint codePoint, ushort glyphId, GlyphType glyphType, TextAttributes textAttributes, TextDecorations textDecorations, + ColorFontSupport colorSupport, bool isVerticalLayout, ushort paletteIndex = 0) { + // TODO: When do we require and how do we use the palette index? CompactFontTables tables = this.compactFontTables!; ICffTable cff = tables.Cff; HorizontalMetricsTable htmx = tables.Htmx; @@ -93,15 +100,24 @@ private CffGlyphMetrics CreateCffGlyphMetrics( tsb = vtmx.GetTopSideBearing(glyphId); } - GlyphColor? color = null; - if (glyphType == GlyphType.ColrLayer) + // TODO: Support CFF based COLR glyphs. + // This requires parsing the CFF charstrings to extract the glyph vectors. + SvgTable? svg = tables.Svg; + if ((colorSupport & ColorFontSupport.Svg) == ColorFontSupport.Svg && svg?.ContainsGlyph(glyphId) == true) { - // 0xFFFF is special index meaning use foreground color and thus leave unset - if (paletteIndex != 0xFFFF) - { - CpalTable? cpal = tables.Cpal; - color = cpal?.GetGlyphColor(0, paletteIndex); - } + return new PaintedGlyphMetrics( + this, + glyphId, + codePoint, + new SvgGlyphSource(svg), + bounds, + advanceWidth, + advancedHeight, + lsb, + tsb, + this.UnitsPerEm, + textAttributes, + textDecorations); } return new CffGlyphMetrics( @@ -117,7 +133,6 @@ private CffGlyphMetrics CreateCffGlyphMetrics( this.UnitsPerEm, textAttributes, textDecorations, - glyphType, - color); + glyphType); } } diff --git a/src/SixLabors.Fonts/StreamFontMetrics.TrueType.cs b/src/SixLabors.Fonts/StreamFontMetrics.TrueType.cs index e33738ef6..8e0fc24e8 100644 --- a/src/SixLabors.Fonts/StreamFontMetrics.TrueType.cs +++ b/src/SixLabors.Fonts/StreamFontMetrics.TrueType.cs @@ -2,12 +2,14 @@ // Licensed under the Six Labors Split License. using System.Numerics; +using SixLabors.Fonts.Rendering; using SixLabors.Fonts.Tables.AdvancedTypographic; using SixLabors.Fonts.Tables.General; using SixLabors.Fonts.Tables.General.Colr; using SixLabors.Fonts.Tables.General.Kern; using SixLabors.Fonts.Tables.General.Name; using SixLabors.Fonts.Tables.General.Post; +using SixLabors.Fonts.Tables.General.Svg; using SixLabors.Fonts.Tables.TrueType; using SixLabors.Fonts.Tables.TrueType.Glyphs; using SixLabors.Fonts.Tables.TrueType.Hinting; @@ -96,6 +98,8 @@ private static StreamFontMetrics LoadTrueTypeFont(FontReader reader) ColrTable? colr = reader.TryGetTable(); CpalTable? cpal = reader.TryGetTable(); + SvgTable? svg = reader.TryGetTable(); + TrueTypeFontTables tables = new(cmap, head, hhea, htmx, maxp, name, os2, post, glyf, loca) { Fpgm = fpgm, @@ -109,27 +113,32 @@ private static StreamFontMetrics LoadTrueTypeFont(FontReader reader) GPos = gPos, Colr = colr, Cpal = cpal, + Svg = svg }; return new StreamFontMetrics(tables); } - private TrueTypeGlyphMetrics CreateTrueTypeGlyphMetrics( + private GlyphMetrics CreateTrueTypeGlyphMetrics( in CodePoint codePoint, ushort glyphId, GlyphType glyphType, TextAttributes textAttributes, TextDecorations textDecorations, + ColorFontSupport colorSupport, bool isVerticalLayout, ushort paletteIndex = 0) { + // TODO: When do we require and how do we use the palette index? TrueTypeFontTables tables = this.trueTypeFontTables!; GlyphTable glyf = tables.Glyf; HorizontalMetricsTable htmx = tables.Htmx; VerticalMetricsTable? vtmx = tables.Vmtx; GlyphVector vector = glyf.GetGlyph(glyphId); + Bounds bounds = vector.Bounds; + ushort advanceWidth = htmx.GetAdvancedWidth(glyphId); short lsb = htmx.GetLeftSideBearing(glyphId); @@ -142,15 +151,63 @@ private TrueTypeGlyphMetrics CreateTrueTypeGlyphMetrics( tsb = vtmx.GetTopSideBearing(glyphId); } - GlyphColor? color = null; - if (glyphType == GlyphType.ColrLayer) + ColrTable? colr = tables.Colr; + if ((colorSupport & ColorFontSupport.ColrV1) == ColorFontSupport.ColrV1 && colr?.ContainsColorV1Glyph(glyphId) == true) { - // 0xFFFF is special index meaning use foreground color and thus leave unset - if (paletteIndex != 0xFFFF) - { - CpalTable? cpal = tables.Cpal; - color = cpal?.GetGlyphColor(0, paletteIndex); - } + CpalTable? cpal = tables.Cpal; + ColrV1GlyphSource glyphSource = new(colr, cpal, i => glyf.GetGlyph(i)); + + return new PaintedGlyphMetrics( + this, + glyphId, + codePoint, + glyphSource, + bounds, + advanceWidth, + advancedHeight, + lsb, + tsb, + this.UnitsPerEm, + textAttributes, + textDecorations); + } + + if ((colorSupport & ColorFontSupport.ColrV0) == ColorFontSupport.ColrV0 && colr?.ContainsColorV0Glyph(glyphId) == true) + { + CpalTable? cpal = tables.Cpal; + ColrV0GlyphSource glyphSource = new(colr, cpal, i => glyf.GetGlyph(i)); + + return new PaintedGlyphMetrics( + this, + glyphId, + codePoint, + glyphSource, + bounds, + advanceWidth, + advancedHeight, + lsb, + tsb, + this.UnitsPerEm, + textAttributes, + textDecorations); + } + + SvgTable? svg = tables.Svg; + if ((colorSupport & ColorFontSupport.Svg) == ColorFontSupport.Svg && svg?.ContainsGlyph(glyphId) == true) + { + return new PaintedGlyphMetrics( + this, + glyphId, + codePoint, + new SvgGlyphSource(svg), + bounds, + advanceWidth, + advancedHeight, + lsb, + tsb, + this.UnitsPerEm, + textAttributes, + textDecorations); } return new TrueTypeGlyphMetrics( @@ -165,7 +222,6 @@ private TrueTypeGlyphMetrics CreateTrueTypeGlyphMetrics( this.UnitsPerEm, textAttributes, textDecorations, - glyphType, - color); + glyphType); } } diff --git a/src/SixLabors.Fonts/StreamFontMetrics.cs b/src/SixLabors.Fonts/StreamFontMetrics.cs index 014b0fcb0..5226cc234 100644 --- a/src/SixLabors.Fonts/StreamFontMetrics.cs +++ b/src/SixLabors.Fonts/StreamFontMetrics.cs @@ -8,7 +8,6 @@ using SixLabors.Fonts.Tables.AdvancedTypographic; using SixLabors.Fonts.Tables.Cff; using SixLabors.Fonts.Tables.General; -using SixLabors.Fonts.Tables.General.Colr; using SixLabors.Fonts.Tables.General.Kern; using SixLabors.Fonts.Tables.General.Post; using SixLabors.Fonts.Tables.TrueType; @@ -29,8 +28,7 @@ internal partial class StreamFontMetrics : FontMetrics private readonly OutlineType outlineType; // https://docs.microsoft.com/en-us/typography/opentype/spec/otff#font-tables - private readonly ConcurrentDictionary<(int CodePoint, ushort Id, TextAttributes Attributes, bool IsVerticalLayout), GlyphMetrics[]> glyphCache; - private readonly ConcurrentDictionary<(int CodePoint, ushort Id, TextAttributes Attributes, bool IsVerticalLayout), GlyphMetrics[]>? colorGlyphCache; + private readonly ConcurrentDictionary<(int CodePoint, ushort Id, TextAttributes Attributes, ColorFontSupport ColorSupport, bool IsVerticalLayout), GlyphMetrics> glyphCache; private readonly ConcurrentDictionary<(int CodePoint, int NextCodePoint), (bool Success, ushort GlyphId, bool SkipNextCodePoint)> glyphIdCache; private readonly ConcurrentDictionary codePointCache; private readonly FontDescription description; @@ -64,10 +62,6 @@ internal StreamFontMetrics(TrueTypeFontTables tables) this.glyphIdCache = new(); this.codePointCache = new(); this.glyphCache = new(); - if (tables.Colr is not null) - { - this.colorGlyphCache = new(); - } (HorizontalMetrics HorizontalMetrics, VerticalMetrics VerticalMetrics) metrics = this.Initialize(tables); this.horizontalMetrics = metrics.HorizontalMetrics; @@ -86,10 +80,6 @@ internal StreamFontMetrics(CompactFontTables tables) this.glyphIdCache = new(); this.codePointCache = new(); this.glyphCache = new(); - if (tables.Colr is not null) - { - this.colorGlyphCache = new(); - } (HorizontalMetrics HorizontalMetrics, VerticalMetrics VerticalMetrics) metrics = this.Initialize(tables); this.horizontalMetrics = metrics.HorizontalMetrics; @@ -226,44 +216,37 @@ public override bool TryGetGlyphMetrics( TextDecorations textDecorations, LayoutMode layoutMode, ColorFontSupport support, - [NotNullWhen(true)] out IReadOnlyList? metrics) + [NotNullWhen(true)] out GlyphMetrics? metrics) { // We return metrics for the special glyph representing a missing character, commonly known as .notdef. this.TryGetGlyphId(codePoint, out ushort glyphId); metrics = this.GetGlyphMetrics(codePoint, glyphId, textAttributes, textDecorations, layoutMode, support); - return metrics.Any(); + return metrics != null; } /// - internal override IReadOnlyList GetGlyphMetrics( + internal override GlyphMetrics GetGlyphMetrics( CodePoint codePoint, ushort glyphId, TextAttributes textAttributes, TextDecorations textDecorations, LayoutMode layoutMode, ColorFontSupport support) - { - if (support == ColorFontSupport.MicrosoftColrFormat - && this.TryGetColoredMetrics(codePoint, glyphId, textAttributes, textDecorations, layoutMode, out GlyphMetrics[]? metrics)) - { - return metrics; - } // We overwrite the cache entry for this type should the attributes change. - return this.glyphCache.GetOrAdd( - CreateCacheKey(in codePoint, glyphId, textAttributes, layoutMode), - static (key, arg) => new[] - { - arg.Item3.CreateGlyphMetrics( - in arg.codePoint, - key.Id, - key.Id == 0 ? GlyphType.Fallback : GlyphType.Standard, - key.Attributes, - arg.textDecorations, - key.IsVerticalLayout) - }, + => this.glyphCache.GetOrAdd( + CreateCacheKey(in codePoint, glyphId, textAttributes, support, layoutMode), + static (key, arg) => + + arg.Item3.CreateGlyphMetrics( + in arg.codePoint, + key.Id, + key.Id == 0 ? GlyphType.Fallback : GlyphType.Standard, + key.Attributes, + arg.textDecorations, + key.ColorSupport, + key.IsVerticalLayout), (textDecorations, codePoint, this)); - } /// public override IReadOnlyList GetAvailableCodePoints() @@ -356,7 +339,7 @@ internal override void UpdatePositions(GlyphPositioningCollection collection) public static StreamFontMetrics LoadFont(string path) { using FileStream fs = File.OpenRead(path); - using var reader = new FontReader(fs); + using FontReader reader = new(fs); return LoadFont(reader); } @@ -380,7 +363,7 @@ public static StreamFontMetrics LoadFont(string path, long offset) /// a . public static StreamFontMetrics LoadFont(Stream stream) { - using var reader = new FontReader(stream); + using FontReader reader = new(stream); return LoadFont(reader); } @@ -546,9 +529,9 @@ public static StreamFontMetrics[] LoadFontCollection(string path) public static StreamFontMetrics[] LoadFontCollection(Stream stream) { long startPos = stream.Position; - var reader = new BigEndianBinaryReader(stream, true); - var ttcHeader = TtcHeader.Read(reader); - var fonts = new StreamFontMetrics[(int)ttcHeader.NumFonts]; + BigEndianBinaryReader reader = new(stream, true); + TtcHeader ttcHeader = TtcHeader.Read(reader); + StreamFontMetrics[] fonts = new StreamFontMetrics[(int)ttcHeader.NumFonts]; for (int i = 0; i < ttcHeader.NumFonts; ++i) { @@ -559,54 +542,13 @@ public static StreamFontMetrics[] LoadFontCollection(Stream stream) return fonts; } - private static (int CodePoint, ushort Id, TextAttributes Attributes, bool IsVerticalLayout) CreateCacheKey( + private static (int CodePoint, ushort Id, TextAttributes Attributes, ColorFontSupport ColorSupport, bool IsVerticalLayout) CreateCacheKey( in CodePoint codePoint, ushort glyphId, TextAttributes textAttributes, + ColorFontSupport colorSupport, LayoutMode layoutMode) - => (codePoint.Value, glyphId, textAttributes, AdvancedTypographicUtils.IsVerticalGlyph(codePoint, layoutMode)); - - private bool TryGetColoredMetrics( - CodePoint codePoint, - ushort glyphId, - TextAttributes textAttributes, - TextDecorations textDecorations, - LayoutMode layoutMode, - [NotNullWhen(true)] out GlyphMetrics[]? metrics) - { - ColrTable? colr = this.outlineType == OutlineType.TrueType - ? this.trueTypeFontTables!.Colr - : this.compactFontTables!.Colr; - - if (colr == null || this.colorGlyphCache == null) - { - metrics = null; - return false; - } - - // We overwrite the cache entry for this type should the attributes change. - metrics = this.colorGlyphCache.GetOrAdd( - CreateCacheKey(in codePoint, glyphId, textAttributes, layoutMode), - (key, args) => - { - GlyphMetrics[] m = Array.Empty(); - Span indexes = colr.GetLayers(key.Id); - if (indexes.Length > 0) - { - m = new GlyphMetrics[indexes.Length]; - for (int i = 0; i < indexes.Length; i++) - { - LayerRecord layer = indexes[i]; - m[i] = args.Item2.CreateGlyphMetrics(in args.codePoint, layer.GlyphId, GlyphType.ColrLayer, key.Attributes, textDecorations, key.IsVerticalLayout, layer.PaletteIndex); - } - } - - return m; - }, - (codePoint, this)); - - return metrics.Length > 0; - } + => (codePoint.Value, glyphId, textAttributes, colorSupport, AdvancedTypographicUtils.IsVerticalGlyph(codePoint, layoutMode)); private GlyphMetrics CreateGlyphMetrics( in CodePoint codePoint, @@ -614,12 +556,13 @@ private GlyphMetrics CreateGlyphMetrics( GlyphType glyphType, TextAttributes textAttributes, TextDecorations textDecorations, + ColorFontSupport colorSupport, bool isVerticalLayout, ushort paletteIndex = 0) => this.outlineType switch { - OutlineType.TrueType => this.CreateTrueTypeGlyphMetrics(in codePoint, glyphId, glyphType, textAttributes, textDecorations, isVerticalLayout, paletteIndex), - OutlineType.CFF => this.CreateCffGlyphMetrics(in codePoint, glyphId, glyphType, textAttributes, textDecorations, isVerticalLayout, paletteIndex), + OutlineType.TrueType => this.CreateTrueTypeGlyphMetrics(in codePoint, glyphId, glyphType, textAttributes, textDecorations, colorSupport, isVerticalLayout, paletteIndex), + OutlineType.CFF => this.CreateCffGlyphMetrics(in codePoint, glyphId, glyphType, textAttributes, textDecorations, colorSupport, isVerticalLayout, paletteIndex), _ => throw new NotSupportedException(), }; } diff --git a/src/SixLabors.Fonts/SystemFontCollection.cs b/src/SixLabors.Fonts/SystemFontCollection.cs index 877b2cac6..33e308a23 100644 --- a/src/SixLabors.Fonts/SystemFontCollection.cs +++ b/src/SixLabors.Fonts/SystemFontCollection.cs @@ -108,7 +108,7 @@ public SystemFontCollection() public IEnumerable SearchDirectories => this.searchDirectories; /// - public FontFamily Get(string name) => this.Get(name, CultureInfo.InvariantCulture); + public FontFamily Get(string name) => this.GetByCulture(name, CultureInfo.InvariantCulture); /// public bool TryGet(string name, out FontFamily family) @@ -119,12 +119,12 @@ public IEnumerable GetByCulture(CultureInfo culture) => this.collection.GetByCulture(culture); /// - public FontFamily Get(string name, CultureInfo culture) - => this.collection.Get(name, culture); + public FontFamily GetByCulture(string name, CultureInfo culture) + => this.collection.GetByCulture(name, culture); /// - public bool TryGet(string name, CultureInfo culture, out FontFamily family) - => this.collection.TryGet(name, culture, out family); + public bool TryGetByCulture(string name, CultureInfo culture, out FontFamily family) + => this.collection.TryGetByCulture(name, culture, out family); /// bool IReadOnlyFontMetricsCollection.TryGetMetrics(string name, CultureInfo culture, FontStyle style, [NotNullWhen(true)] out FontMetrics? metrics) diff --git a/src/SixLabors.Fonts/SystemFonts.cs b/src/SixLabors.Fonts/SystemFonts.cs index cedd71c1d..cb1b84a5d 100644 --- a/src/SixLabors.Fonts/SystemFonts.cs +++ b/src/SixLabors.Fonts/SystemFonts.cs @@ -23,7 +23,7 @@ public static class SystemFonts public static IEnumerable Families => Collection.Families; /// - public static FontFamily Get(string name) => Get(name, CultureInfo.InvariantCulture); + public static FontFamily Get(string name) => GetByCulture(name, CultureInfo.InvariantCulture); /// public static bool TryGet(string fontFamily, out FontFamily family) @@ -52,13 +52,13 @@ public static Font CreateFont(string name, float size, FontStyle style) public static IEnumerable GetByCulture(CultureInfo culture) => Collection.GetByCulture(culture); - /// - public static FontFamily Get(string fontFamily, CultureInfo culture) - => Collection.Get(fontFamily, culture); + /// + public static FontFamily GetByCulture(string fontFamily, CultureInfo culture) + => Collection.GetByCulture(fontFamily, culture); - /// - public static bool TryGet(string fontFamily, CultureInfo culture, out FontFamily family) - => Collection.TryGet(fontFamily, culture, out family); + /// + public static bool TryGetByCulture(string fontFamily, CultureInfo culture, out FontFamily family) + => Collection.TryGetByCulture(fontFamily, culture, out family); /// /// Create a new instance of the for the named font family with regular styling. @@ -68,7 +68,7 @@ public static bool TryGet(string fontFamily, CultureInfo culture, out FontFamily /// The size of the font in PT units. /// The new . public static Font CreateFont(string name, CultureInfo culture, float size) - => Collection.Get(name, culture).CreateFont(size); + => Collection.GetByCulture(name, culture).CreateFont(size); /// /// Create a new instance of the for the named font family. @@ -79,5 +79,5 @@ public static Font CreateFont(string name, CultureInfo culture, float size) /// The font style. /// The new . public static Font CreateFont(string name, CultureInfo culture, float size, FontStyle style) - => Collection.Get(name, culture).CreateFont(size, style); + => Collection.GetByCulture(name, culture).CreateFont(size, style); } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/FeatureTags.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/FeatureTags.cs index a86552431..42c35f480 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/FeatureTags.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/FeatureTags.cs @@ -91,7 +91,7 @@ public enum FeatureTags : uint /// /// Conjunct Form After Ro. Shortcode: cfar. - /// Substitutes alternate below-base or post-base forms in Khmer script when occurring after conjoined Ro (“Coeng Ra”). + /// Substitutes alternate below-base or post-base forms in Khmer script when occurring after conjoined Ro ("Coeng Ra"). /// ConjunctFormAfterRo = 0x63666172U, @@ -217,13 +217,13 @@ public enum FeatureTags : uint /// /// Fractions. Shortcode: frac. - /// Replaces figures separated by a slash with “common” (diagonal) fractions. + /// Replaces figures separated by a slash with "common" (diagonal) fractions. /// Fractions = 0x66726163U, /// /// Full Widths. Shortcode: fwid. - /// Replaces glyphs set on other widths with glyphs set on full (usually em) widths. In a CJKV font, this may include “lower ASCII” Latin characters and various symbols. + /// Replaces glyphs set on other widths with glyphs set on full (usually em) widths. In a CJKV font, this may include "lower ASCII" Latin characters and various symbols. /// In a European font, this feature replaces proportionally-spaced glyphs with monospaced glyphs, which are generally set on widths of 0.6 em. /// FullWidths = 0x66776964U, @@ -249,7 +249,7 @@ public enum FeatureTags : uint /// /// Historical Forms. Shortcode: hist. /// Some letterforms were in common use in the past, but appear anachronistic today. The best-known example is the long form of s; others would include the old Fraktur k. - /// Some fonts include the historical forms as alternates, so they can be used for a “period” effect. This feature replaces the default (current) forms with the historical alternates. + /// Some fonts include the historical forms as alternates, so they can be used for a "period" effect. This feature replaces the default (current) forms with the historical alternates. /// While some ligatures are also used for historical effect, this feature deals only with single characters. /// HistoricalForms = 0x68697374U, @@ -262,7 +262,7 @@ public enum FeatureTags : uint /// /// Historical Ligatures. Shortcode: hlig. - /// Some ligatures were in common use in the past, but appear anachronistic today. Some fonts include the historical forms as alternates, so they can be used for a “period” effect. + /// Some ligatures were in common use in the past, but appear anachronistic today. Some fonts include the historical forms as alternates, so they can be used for a "period" effect. /// This feature replaces the default (current) forms with the historical alternates. /// HistoricalLigatures = 0x686C6967U, @@ -277,7 +277,7 @@ public enum FeatureTags : uint /// /// Hojo Kanji Forms (JIS X 0212-1990 Kanji Forms). Shortcode: hojo. - /// The JIS X 0212-1990 (aka, “Hojo Kanji”) and JIS X 0213:2004 character sets overlap significantly. + /// The JIS X 0212-1990 (aka, "Hojo Kanji") and JIS X 0213:2004 character sets overlap significantly. /// In some cases their prototypical glyphs differ. When building fonts that support both JIS X 0212-1990 and JIS X 0213:2004 (such as those supporting the Adobe-Japan 1-6 character collection), /// it is recommended that JIS X 0213:2004 forms be preferred as the encoded form. The 'hojo' feature is used to access the JIS X 0212-1990 glyphs for the cases when the JIS X 0213:2004 form is encoded. /// @@ -344,7 +344,7 @@ public enum FeatureTags : uint /// Kerning. Shortcode: kern. /// Adjusts amount of space between glyphs, generally to provide optically consistent spacing between glyphs. /// Although a well-designed typeface has consistent inter-glyph spacing overall, some glyph combinations require adjustment for improved legibility. - /// Besides standard adjustment in the horizontal direction, this feature can supply size-dependent kerning data via device tables, “cross-stream” kerning in the Y text direction, + /// Besides standard adjustment in the horizontal direction, this feature can supply size-dependent kerning data via device tables, "cross-stream" kerning in the Y text direction, /// and adjustment of glyph placement independent of the advance adjustment. Note that this feature may apply to runs of more than two glyphs, and would not be used in monospaced fonts. /// Also note that this feature does not apply to text set vertically. /// @@ -379,7 +379,7 @@ public enum FeatureTags : uint /// Localized Forms. Shortcode: locl. /// Many scripts used to write multiple languages over wide geographical areas have developed localized variant forms of specific letters, /// which are used by individual literary communities. For example, a number of letters in the Bulgarian and Serbian alphabets have forms distinct from their Russian counterparts and from each other. - /// In some cases the localized form differs only subtly from the script “norm”, in others the forms are radically distinct. This feature enables localized forms of glyphs to be substituted for default forms. + /// In some cases the localized form differs only subtly from the script "norm", in others the forms are radically distinct. This feature enables localized forms of glyphs to be substituted for default forms. /// LocalizedForms = 0x6C6F636CU, @@ -482,7 +482,7 @@ public enum FeatureTags : uint /// Ornaments. Shortcode: ornm. /// This is a dual-function feature, which uses two input methods to give the user access to ornament glyphs (e.g. fleurons, dingbats and border elements) in the font. /// One method replaces the bullet character with a selection from the full set of available ornaments; - /// the other replaces specific “lower ASCII” characters with ornaments assigned to them. The first approach supports the general or browsing user; + /// the other replaces specific "lower ASCII" characters with ornaments assigned to them. The first approach supports the general or browsing user; /// the second supports the power user. /// Ornaments = 0x6F726E6DU, @@ -610,7 +610,7 @@ public enum FeatureTags : uint /// /// Ruby Notation Forms. Shortcode: ruby. /// Japanese typesetting often uses smaller kana glyphs, generally in superscripted form, to clarify the meaning of kanji which may be unfamiliar to the reader. - /// These are called “ruby”, from the old typesetting term for four-point-sized type. This feature identifies glyphs in the font which have been designed for this use, + /// These are called "ruby", from the old typesetting term for four-point-sized type. This feature identifies glyphs in the font which have been designed for this use, /// substituting them for the default designs. /// RubyNotationForms = 0x72756279U, @@ -654,7 +654,7 @@ public enum FeatureTags : uint /// /// Simplified Forms. Shortcode: smpl. - /// Replaces “traditional” Chinese or Japanese forms with the corresponding “simplified” forms. + /// Replaces "traditional" Chinese or Japanese forms with the corresponding "simplified" forms. /// SimplifiedForms = 0x736D706CU, @@ -708,7 +708,7 @@ public enum FeatureTags : uint /// /// Traditional Name Forms. Shortcode: tnam. - /// Replaces “simplified” Japanese kanji forms with the corresponding “traditional” forms. This is equivalent to the Traditional Forms feature, + /// Replaces "simplified" Japanese kanji forms with the corresponding "traditional" forms. This is equivalent to the Traditional Forms feature, /// but explicitly limited to the traditional forms considered proper for use in personal names (as many as 205 glyphs in some fonts). /// TraditionalNameForms = 0x746E616DU, @@ -784,7 +784,7 @@ public enum FeatureTags : uint /// Adjusts amount of space between glyphs, generally to provide optically consistent spacing between glyphs. /// Although a well-designed typeface has consistent inter-glyph spacing overall, some glyph combinations require adjustment for improved legibility. /// Besides standard adjustment in the vertical direction, this feature can supply size-dependent kerning data via device tables, - /// “cross-stream” kerning in the X text direction, and adjustment of glyph placement independent of the advance adjustment. + /// "cross-stream" kerning in the X text direction, and adjustment of glyph placement independent of the advance adjustment. /// Note that this feature may apply to runs of more than two glyphs, and would not be used in monospaced fonts. Also note that this feature applies only to text set vertically. /// VerticalKerning = 0x766B726EU, diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/AnchorTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/AnchorTable.cs index 870e62b3e..bc12a7722 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/AnchorTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/AnchorTable.cs @@ -120,15 +120,10 @@ public override AnchorXY GetAnchor(FontMetrics fontMetrics, GlyphShapingData dat TextDecorations textDecorations = data.TextRun.TextDecorations; LayoutMode layoutMode = collection.TextOptions.LayoutMode; ColorFontSupport colorFontSupport = collection.TextOptions.ColorFontSupport; - if (fontMetrics.TryGetGlyphMetrics(data.CodePoint, textAttributes, textDecorations, layoutMode, colorFontSupport, out IReadOnlyList? metrics)) + if (fontMetrics.TryGetGlyphMetrics(data.CodePoint, textAttributes, textDecorations, layoutMode, colorFontSupport, out GlyphMetrics? metrics)) { - foreach (GlyphMetrics metric in metrics) + if (metrics is TrueTypeGlyphMetrics ttmetric) { - if (metric is not TrueTypeGlyphMetrics ttmetric) - { - break; - } - IList points = ttmetric.GetOutline().ControlPoints; if (this.anchorPointIndex < points.Count) { diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType9SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType9SubTable.cs index 9585fb22a..0e6b407b1 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType9SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType9SubTable.cs @@ -6,7 +6,7 @@ namespace SixLabors.Fonts.Tables.AdvancedTypographic.GPos; /// /// This lookup provides a mechanism whereby any other lookup type’s subtables are stored at a 32-bit offset location in the GPOS table. /// This is needed if the total size of the subtables exceeds the 16-bit limits of the various other offsets in the GPOS table. -/// In this specification, the subtable stored at the 32-bit offset location is termed the “extension” subtable. +/// In this specification, the subtable stored at the 32-bit offset location is termed the "extension" subtable. /// /// internal static class LookupType9SubTable diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HangulShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HangulShaper.cs index 5888e94ae..32dec1314 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HangulShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HangulShaper.cs @@ -231,8 +231,8 @@ private int DecomposeGlyph(GlyphSubstitutionCollection collection, GlyphShapingD if (t <= TBase) { Span ii = stackalloc ushort[2]; - ii[0] = ljmo; ii[1] = vjmo; + ii[0] = ljmo; collection.Replace(index, ii, FeatureTags.GlyphCompositionDecomposition); collection.EnableShapingFeature(index, LjmoTag); @@ -360,14 +360,11 @@ private static void ReOrderToneMark(GlyphSubstitutionCollection collection, Glyp TextDecorations textDecorations = data.TextRun.TextDecorations; LayoutMode layoutMode = collection.TextOptions.LayoutMode; ColorFontSupport colorFontSupport = collection.TextOptions.ColorFontSupport; - if (fontMetrics.TryGetGlyphMetrics(data.CodePoint, textAttributes, textDecorations, layoutMode, colorFontSupport, out IReadOnlyList? metrics)) + if (fontMetrics.TryGetGlyphMetrics(data.CodePoint, textAttributes, textDecorations, layoutMode, colorFontSupport, out GlyphMetrics? metrics)) { - foreach (GlyphMetrics gm in metrics) + if (metrics.AdvanceWidth == 0) { - if (gm.AdvanceWidth == 0) - { - return; - } + return; } } @@ -387,15 +384,11 @@ private int InsertDottedCircle(GlyphSubstitutionCollection collection, GlyphShap TextDecorations textDecorations = data.TextRun.TextDecorations; LayoutMode layoutMode = collection.TextOptions.LayoutMode; ColorFontSupport colorFontSupport = collection.TextOptions.ColorFontSupport; - if (fontMetrics.TryGetGlyphMetrics(data.CodePoint, textAttributes, textDecorations, layoutMode, colorFontSupport, out IReadOnlyList? metrics)) + if (fontMetrics.TryGetGlyphMetrics(data.CodePoint, textAttributes, textDecorations, layoutMode, colorFontSupport, out GlyphMetrics? metrics)) { - foreach (GlyphMetrics gm in metrics) + if (metrics.AdvanceWidth != 0) { - if (gm.AdvanceWidth != 0) - { - after = true; - break; - } + after = true; } } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/IndicShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/IndicShaper.cs index b28e802c7..5f4a0c4ef 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/IndicShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/IndicShaper.cs @@ -982,8 +982,8 @@ private void FinalReorder(IGlyphShapingCollection collection, int index, int cou // If a pre-base matra character had been reordered before applying basic // features, the glyph can be moved closer to the main consonant based on // whether half-forms had been formed. Actual position for the matra is - // defined as “after last standalone halant glyph, after initial matra - // position and before the main consonant”. If ZWJ or ZWNJ follow this + // defined as "after last standalone halant glyph, after initial matra + // position and before the main consonant". If ZWJ or ZWNJ follow this // halant, position is moved after it. // // Otherwise there can't be any pre-base matra characters. diff --git a/src/SixLabors.Fonts/Tables/Cff/CffBoundsFinder.cs b/src/SixLabors.Fonts/Tables/Cff/CffBoundsFinder.cs index c40104de0..3e46f38ee 100644 --- a/src/SixLabors.Fonts/Tables/Cff/CffBoundsFinder.cs +++ b/src/SixLabors.Fonts/Tables/Cff/CffBoundsFinder.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Numerics; +using SixLabors.Fonts.Rendering; namespace SixLabors.Fonts.Tables.Cff; @@ -63,6 +64,16 @@ public void EndText() } } + public void BeginLayer(Paint? paint, FillRule fillRule, ClipQuad? clipBounds) + { + // Do nothing. + } + + public void EndLayer() + { + // Do nothing. + } + public void LineTo(Vector2 point) { this.currentXY = point; @@ -81,6 +92,14 @@ public void MoveTo(Vector2 point) this.UpdateMinMax(point.X, point.Y); } + public void ArcTo(float radiusX, float radiusY, float xAxisRotation, bool largeArc, bool sweep, Vector2 point) + { + // TODO: check this. I feel like we should have to implement it. + this.currentXY = point; + this.UpdateMinMax(point.X, point.Y); + this.open = true; + } + public void CubicBezierTo(Vector2 secondControlPoint, Vector2 thirdControlPoint, Vector2 point) { float eachstep = 1F / this.nsteps; diff --git a/src/SixLabors.Fonts/Tables/Cff/CffEvaluationEngine.cs b/src/SixLabors.Fonts/Tables/Cff/CffEvaluationEngine.cs index a3b48bedf..dabdda4ce 100644 --- a/src/SixLabors.Fonts/Tables/Cff/CffEvaluationEngine.cs +++ b/src/SixLabors.Fonts/Tables/Cff/CffEvaluationEngine.cs @@ -3,6 +3,7 @@ using System.Numerics; using System.Runtime.CompilerServices; +using SixLabors.Fonts.Rendering; namespace SixLabors.Fonts.Tables.Cff; diff --git a/src/SixLabors.Fonts/Tables/Cff/CffGlyphData.cs b/src/SixLabors.Fonts/Tables/Cff/CffGlyphData.cs index a5e051c83..1bb5bc54e 100644 --- a/src/SixLabors.Fonts/Tables/Cff/CffGlyphData.cs +++ b/src/SixLabors.Fonts/Tables/Cff/CffGlyphData.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Numerics; +using SixLabors.Fonts.Rendering; namespace SixLabors.Fonts.Tables.Cff; diff --git a/src/SixLabors.Fonts/Tables/Cff/CffGlyphMetrics.cs b/src/SixLabors.Fonts/Tables/Cff/CffGlyphMetrics.cs index 11be31ca8..ca952eaf5 100644 --- a/src/SixLabors.Fonts/Tables/Cff/CffGlyphMetrics.cs +++ b/src/SixLabors.Fonts/Tables/Cff/CffGlyphMetrics.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Numerics; +using SixLabors.Fonts.Rendering; using SixLabors.Fonts.Unicode; namespace SixLabors.Fonts.Tables.Cff; @@ -26,8 +27,7 @@ internal CffGlyphMetrics( ushort unitsPerEM, TextAttributes textAttributes, TextDecorations textDecorations, - GlyphType glyphType = GlyphType.Standard, - GlyphColor? glyphColor = null) + GlyphType glyphType) : base( fontMetrics, glyphId, @@ -40,8 +40,7 @@ internal CffGlyphMetrics( unitsPerEM, textAttributes, textDecorations, - glyphType, - glyphColor) + glyphType) => this.glyphData = glyphData; internal CffGlyphMetrics( @@ -58,8 +57,7 @@ internal CffGlyphMetrics( Vector2 offset, Vector2 scaleFactor, TextRun textRun, - GlyphType glyphType = GlyphType.Standard, - GlyphColor? glyphColor = null) + GlyphType glyphType) : base( fontMetrics, glyphId, @@ -73,8 +71,7 @@ internal CffGlyphMetrics( offset, scaleFactor, textRun, - glyphType, - glyphColor) + glyphType) => this.glyphData = glyphData; /// @@ -93,11 +90,16 @@ internal override GlyphMetrics CloneForRendering(TextRun textRun) this.Offset, this.ScaleFactor, textRun, - this.GlyphType, - this.GlyphColor); + this.GlyphType); /// - internal override void RenderTo(IGlyphRenderer renderer, Vector2 location, Vector2 offset, GlyphLayoutMode mode, TextOptions options) + internal override void RenderTo( + IGlyphRenderer renderer, + int graphemeIndex, + Vector2 location, + Vector2 offset, + GlyphLayoutMode mode, + TextOptions options) { // https://www.unicode.org/faq/unsup_char.html if (ShouldSkipGlyphRendering(this.CodePoint)) @@ -118,25 +120,19 @@ internal override void RenderTo(IGlyphRenderer renderer, Vector2 location, Vecto Matrix3x2 rotation = GetRotationMatrix(mode); FontRectangle box = this.GetBoundingBox(mode, renderLocation, scaledPPEM); - GlyphRendererParameters parameters = new(this, this.TextRun, pointSize, dpi, mode); + GlyphRendererParameters parameters = new(this, this.TextRun, pointSize, dpi, mode, graphemeIndex); if (renderer.BeginGlyph(in box, in parameters)) { if (!UnicodeUtility.ShouldRenderWhiteSpaceOnly(this.CodePoint)) { - if (this.GlyphColor.HasValue && renderer is IColorGlyphRenderer colorSurface) - { - colorSurface.SetColor(this.GlyphColor.Value); - } - Vector2 scale = new Vector2(scaledPPEM) / this.ScaleFactor; Vector2 scaledOffset = this.Offset * scale; this.glyphData.RenderTo(renderer, renderLocation, scale, scaledOffset, rotation); } - this.RenderDecorationsTo(renderer, location, mode, rotation, scaledPPEM); + renderer.EndGlyph(); + this.RenderDecorationsTo(renderer, location, mode, rotation, scaledPPEM, options); } - - renderer.EndGlyph(); } } diff --git a/src/SixLabors.Fonts/Tables/Cff/CffParser.cs b/src/SixLabors.Fonts/Tables/Cff/CffParser.cs index 1a8020030..d0701d8b9 100644 --- a/src/SixLabors.Fonts/Tables/Cff/CffParser.cs +++ b/src/SixLabors.Fonts/Tables/Cff/CffParser.cs @@ -566,10 +566,10 @@ private CffGlyphData[] ReadCharStringsIndex( // in conjunction with CFF. // Type 1 charstrings are documented in - // the “Adobe Type 1 Font Format” published by Addison - Wesley. + // the "Adobe Type 1 Font Format" published by Addison - Wesley. // Type 2 charstrings are described in Adobe Technical Note #5177: - // “Type 2 Charstring Format.” Other charstring types may also be + // "Type 2 Charstring Format." Other charstring types may also be // supported by this method. reader.BaseStream.Position = this.offset + this.charStringsOffset; if (!TryReadIndexDataOffsets(reader, out CffIndexOffset[]? offsets)) diff --git a/src/SixLabors.Fonts/Tables/Cff/CompactFontTables.cs b/src/SixLabors.Fonts/Tables/Cff/CompactFontTables.cs index c6cf1af1e..ad3a439f4 100644 --- a/src/SixLabors.Fonts/Tables/Cff/CompactFontTables.cs +++ b/src/SixLabors.Fonts/Tables/Cff/CompactFontTables.cs @@ -7,6 +7,7 @@ using SixLabors.Fonts.Tables.General.Kern; using SixLabors.Fonts.Tables.General.Name; using SixLabors.Fonts.Tables.General.Post; +using SixLabors.Fonts.Tables.General.Svg; namespace SixLabors.Fonts.Tables.Cff; @@ -66,6 +67,8 @@ public CompactFontTables( public VerticalMetricsTable? Vmtx { get; set; } + public SvgTable? Svg { get; set; } + // Tables Related to CFF Outlines // +------+----------------------------------+ // | Tag | Name | diff --git a/src/SixLabors.Fonts/Tables/Cff/TransformingGlyphRenderer.cs b/src/SixLabors.Fonts/Tables/Cff/TransformingGlyphRenderer.cs index 807502940..4b2574949 100644 --- a/src/SixLabors.Fonts/Tables/Cff/TransformingGlyphRenderer.cs +++ b/src/SixLabors.Fonts/Tables/Cff/TransformingGlyphRenderer.cs @@ -3,6 +3,7 @@ using System.Numerics; using System.Runtime.CompilerServices; +using SixLabors.Fonts.Rendering; namespace SixLabors.Fonts.Tables.Cff; @@ -84,6 +85,12 @@ public void MoveTo(Vector2 point) this.IsOpen = true; } + public void ArcTo(float radiusX, float radiusY, float rotationDegrees, bool largeArc, bool sweep, Vector2 point) + { + this.IsOpen = true; + this.renderer.ArcTo(radiusX * this.scale.X, radiusY * this.scale.Y, rotationDegrees, largeArc, sweep, this.Transform(point)); + } + public void CubicBezierTo(Vector2 secondControlPoint, Vector2 thirdControlPoint, Vector2 point) { this.IsOpen = true; @@ -99,10 +106,15 @@ public void QuadraticBezierTo(Vector2 secondControlPoint, Vector2 point) public readonly TextDecorations EnabledDecorations() => this.renderer.EnabledDecorations(); - public void SetDecoration(TextDecorations textDecorations, Vector2 start, Vector2 end, float thickness) + public readonly void SetDecoration(TextDecorations textDecorations, Vector2 start, Vector2 end, float thickness) => this.renderer.SetDecoration(textDecorations, this.Transform(start), this.Transform(end), thickness); [MethodImpl(MethodImplOptions.AggressiveInlining)] private readonly Vector2 Transform(Vector2 point) => (Vector2.Transform((point * this.scale) + this.offset, this.transform) * YInverter) + this.origin; + + public readonly void BeginLayer(Paint? paint, FillRule fillRule, ClipQuad? clipBounds) + => this.renderer.BeginLayer(paint, fillRule, clipBounds); + + public readonly void EndLayer() => this.renderer.EndLayer(); } diff --git a/src/SixLabors.Fonts/Tables/General/CMap/Format12SubTable.cs b/src/SixLabors.Fonts/Tables/General/CMap/Format12SubTable.cs index 2a30b6a91..b4ad0cb5f 100644 --- a/src/SixLabors.Fonts/Tables/General/CMap/Format12SubTable.cs +++ b/src/SixLabors.Fonts/Tables/General/CMap/Format12SubTable.cs @@ -73,7 +73,7 @@ public static IEnumerable Load(IEnumerable enc // uint16 | format | Subtable format; set to 12. // uint16 | reserved | Reserved; set to 0 // uint32 | length | Byte length of this subtable(including the header) - // uint32 | language | For requirements on use of the language field, see “Use of the language field in 'cmap' subtables” in this document. + // uint32 | language | For requirements on use of the language field, see "Use of the language field in 'cmap' subtables" in this document. // uint32 | numGroups | Number of groupings which follow // SequentialMapGroup | groups[numGroups] | Array of SequentialMapGroup records. diff --git a/src/SixLabors.Fonts/Tables/General/CMap/Format14SubTable.cs b/src/SixLabors.Fonts/Tables/General/CMap/Format14SubTable.cs index bf4f26172..10f1a436b 100644 --- a/src/SixLabors.Fonts/Tables/General/CMap/Format14SubTable.cs +++ b/src/SixLabors.Fonts/Tables/General/CMap/Format14SubTable.cs @@ -13,9 +13,9 @@ namespace SixLabors.Fonts.Tables.General.CMap; /// internal sealed class Format14SubTable : CMapSubTable { - private readonly Dictionary variationSelectors; + private readonly Dictionary variationSelectors; - private Format14SubTable(Dictionary variationSelectors, PlatformIDs platform, ushort encoding) + private Format14SubTable(Dictionary variationSelectors, PlatformIDs platform, ushort encoding) : base(platform, encoding, 5) => this.variationSelectors = variationSelectors; @@ -38,8 +38,8 @@ public static IEnumerable Load( uint length = reader.ReadUInt32(); uint numVarSelectorRecords = reader.ReadUInt32(); - var variationSelectors = new Dictionary(); - int[] varSelectors = new int[numVarSelectorRecords]; + var variationSelectors = new Dictionary(); + uint[] varSelectors = new uint[numVarSelectorRecords]; uint[] defaultUVSOffsets = new uint[numVarSelectorRecords]; uint[] nonDefaultUVSOffsets = new uint[numVarSelectorRecords]; for (int i = 0; i < numVarSelectorRecords; ++i) @@ -86,7 +86,7 @@ public static IEnumerable Load( uint numUnicodeValueRanges = reader.ReadUInt32(); for (int n = 0; n < numUnicodeValueRanges; n++) { - int startCode = reader.ReadUInt24(); + uint startCode = reader.ReadUInt24(); selector.DefaultStartCodes.Add(startCode); selector.DefaultEndCodes.Add(startCode + reader.ReadByte()); } @@ -115,7 +115,7 @@ public static IEnumerable Load( uint numUVSMappings = reader.ReadUInt32(); for (int n = 0; n < numUVSMappings; n++) { - int unicodeValue = reader.ReadUInt24(); + uint unicodeValue = reader.ReadUInt24(); ushort glyphID = reader.ReadUInt16(); selector.UVSMappings.Add(unicodeValue, glyphID); } @@ -148,10 +148,10 @@ public override IEnumerable GetAvailableCodePoints() public ushort CharacterPairToGlyphId(CodePoint codePoint, ushort defaultGlyphIndex, CodePoint nextCodePoint) { // Only check codepoint if nextCodepoint is a variation selector - if (this.variationSelectors.TryGetValue(nextCodePoint.Value, out VariationSelector? sel)) + if (this.variationSelectors.TryGetValue((uint)nextCodePoint.Value, out VariationSelector? sel)) { // If the sequence is a non-default UVS, return the mapped glyph - if (sel.UVSMappings.TryGetValue(codePoint.Value, out ushort ret)) + if (sel.UVSMappings.TryGetValue((uint)codePoint.Value, out ushort ret)) { return ret; } @@ -167,7 +167,7 @@ public ushort CharacterPairToGlyphId(CodePoint codePoint, ushort defaultGlyphInd // At this point we are neither a non-default UVS nor a default UVS, // but we know the nextCodepoint is a variation selector. Unicode says - // this glyph should be invisible: “no visible rendering for the VS” + // this glyph should be invisible: "no visible rendering for the VS" // (http://unicode.org/faq/unsup_char.html#4) return defaultGlyphIndex; } @@ -178,10 +178,10 @@ public ushort CharacterPairToGlyphId(CodePoint codePoint, ushort defaultGlyphInd private class VariationSelector { - public List DefaultStartCodes { get; } = new List(); + public List DefaultStartCodes { get; } = []; - public List DefaultEndCodes { get; } = new List(); + public List DefaultEndCodes { get; } = []; - public Dictionary UVSMappings { get; } = new Dictionary(); + public Dictionary UVSMappings { get; } = []; } } diff --git a/src/SixLabors.Fonts/Tables/General/CMap/Format4SubTable.cs b/src/SixLabors.Fonts/Tables/General/CMap/Format4SubTable.cs index edbf09345..9f89774fe 100644 --- a/src/SixLabors.Fonts/Tables/General/CMap/Format4SubTable.cs +++ b/src/SixLabors.Fonts/Tables/General/CMap/Format4SubTable.cs @@ -102,7 +102,7 @@ public static IEnumerable Load(IEnumerable enco // -------|----------------------------|------------------------------------------------------------------------ // uint16 | format | Format number is set to 4. // uint16 | length | This is the length in bytes of the subtable. - // uint16 | language | Please see “Note on the language field in 'cmap' subtables“ in this document. + // uint16 | language | Please see "Note on the language field in 'cmap' subtables" in this document. // uint16 | segCountX2 | 2 x segCount. // uint16 | searchRange | 2 x (2**floor(log2(segCount))) // uint16 | entrySelector | log2(searchRange/2) diff --git a/src/SixLabors.Fonts/Tables/General/Colr/Affine2x3.cs b/src/SixLabors.Fonts/Tables/General/Colr/Affine2x3.cs new file mode 100644 index 000000000..f15289d1e --- /dev/null +++ b/src/SixLabors.Fonts/Tables/General/Colr/Affine2x3.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables.General.Colr; + +// Affine matrices used by PaintTransform variants +internal readonly struct Affine2x3 +{ + public readonly float Xx; // Fixed 16.16 + public readonly float Yx; + public readonly float Xy; + public readonly float Yy; + public readonly float Dx; + public readonly float Dy; + + public Affine2x3(float xx, float yx, float xy, float yy, float dx, float dy) + { + this.Xx = xx; + this.Yx = yx; + this.Xy = xy; + this.Yy = yy; + this.Dx = dx; + this.Dy = dy; + } +} diff --git a/src/SixLabors.Fonts/Tables/General/Colr/BaseGlyphList.cs b/src/SixLabors.Fonts/Tables/General/Colr/BaseGlyphList.cs new file mode 100644 index 000000000..7af522a50 --- /dev/null +++ b/src/SixLabors.Fonts/Tables/General/Colr/BaseGlyphList.cs @@ -0,0 +1,41 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables.General.Colr; + +internal sealed class BaseGlyphList +{ + public BaseGlyphList(BaseGlyphPaintRecord[] records) + => this.Records = records; + + public BaseGlyphPaintRecord[] Records { get; } + + public int Count => this.Records.Length; + + public static BaseGlyphList? Load(BigEndianBinaryReader reader, uint offset) + { + if (offset == 0) + { + return null; + } + + reader.Seek(offset, SeekOrigin.Begin); + uint count = reader.ReadUInt32(); + + if (count == 0) + { + return null; + } + + // Offsets are relative to the table start; convert to COLR-relative. + BaseGlyphPaintRecord[] records = new BaseGlyphPaintRecord[count]; + for (int i = 0; i < count; i++) + { + ushort glyphId = reader.ReadUInt16(); + records[i] = new BaseGlyphPaintRecord(glyphId, offset + reader.ReadOffset32()); + } + + // Spec says records are sorted by glyphId; assume font is correct + return new BaseGlyphList(records); + } +} diff --git a/src/SixLabors.Fonts/Tables/General/Colr/BaseGlyphPaintRecord.cs b/src/SixLabors.Fonts/Tables/General/Colr/BaseGlyphPaintRecord.cs new file mode 100644 index 000000000..637442888 --- /dev/null +++ b/src/SixLabors.Fonts/Tables/General/Colr/BaseGlyphPaintRecord.cs @@ -0,0 +1,17 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables.General.Colr; + +internal readonly struct BaseGlyphPaintRecord +{ + public BaseGlyphPaintRecord(ushort glyphId, uint paintOffset) + { + this.GlyphId = glyphId; + this.PaintOffset = paintOffset; + } + + public ushort GlyphId { get; } + + public uint PaintOffset { get; } +} diff --git a/src/SixLabors.Fonts/Tables/General/Colr/BaseGlyphRecord.cs b/src/SixLabors.Fonts/Tables/General/Colr/BaseGlyphRecord.cs index 5e3d0a7e9..78b30f833 100644 --- a/src/SixLabors.Fonts/Tables/General/Colr/BaseGlyphRecord.cs +++ b/src/SixLabors.Fonts/Tables/General/Colr/BaseGlyphRecord.cs @@ -3,7 +3,7 @@ namespace SixLabors.Fonts.Tables.General.Colr; -internal sealed class BaseGlyphRecord +internal readonly struct BaseGlyphRecord { public BaseGlyphRecord(ushort glyphId, ushort firstLayerIndex, ushort layerCount) { @@ -18,16 +18,3 @@ public BaseGlyphRecord(ushort glyphId, ushort firstLayerIndex, ushort layerCount public ushort LayerCount { get; } } - -internal sealed class LayerRecord -{ - public LayerRecord(ushort glyphId, ushort paletteIndex) - { - this.GlyphId = glyphId; - this.PaletteIndex = paletteIndex; - } - - public ushort GlyphId { get; } - - public ushort PaletteIndex { get; } -} diff --git a/src/SixLabors.Fonts/Tables/General/Colr/ClipBox.cs b/src/SixLabors.Fonts/Tables/General/Colr/ClipBox.cs new file mode 100644 index 000000000..9cb0b60a1 --- /dev/null +++ b/src/SixLabors.Fonts/Tables/General/Colr/ClipBox.cs @@ -0,0 +1,10 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables.General.Colr; + +// Abstract ClipBox subtable (format-dispatched). +internal abstract class ClipBox +{ + public abstract Bounds GetBounds(IVariationResolver? varResolver); +} diff --git a/src/SixLabors.Fonts/Tables/General/Colr/ClipBoxFormat1.cs b/src/SixLabors.Fonts/Tables/General/Colr/ClipBoxFormat1.cs new file mode 100644 index 000000000..136c58ee5 --- /dev/null +++ b/src/SixLabors.Fonts/Tables/General/Colr/ClipBoxFormat1.cs @@ -0,0 +1,24 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables.General.Colr; + +// Format 1: int16 edges. +internal sealed class ClipBoxFormat1 : ClipBox +{ + private readonly short xMin; + private readonly short yMin; + private readonly short xMax; + private readonly short yMax; + + public ClipBoxFormat1(short xMin, short yMin, short xMax, short yMax) + { + this.xMin = xMin; + this.yMin = yMin; + this.xMax = xMax; + this.yMax = yMax; + } + + public override Bounds GetBounds(IVariationResolver? varResolver) + => new(this.xMin, this.yMin, this.xMax, this.yMax); +} diff --git a/src/SixLabors.Fonts/Tables/General/Colr/ClipBoxFormat2.cs b/src/SixLabors.Fonts/Tables/General/Colr/ClipBoxFormat2.cs new file mode 100644 index 000000000..6e341819a --- /dev/null +++ b/src/SixLabors.Fonts/Tables/General/Colr/ClipBoxFormat2.cs @@ -0,0 +1,38 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables.General.Colr; + +// Format 2: int16 edges + varIndex per edge. +internal sealed class ClipBoxFormat2 : ClipBox +{ + private readonly short xMin; + private readonly short yMin; + private readonly short xMax; + private readonly short yMax; + private readonly uint varIndexBase; + + public ClipBoxFormat2(short xMin, short yMin, short xMax, short yMax, uint varIndexBase) + { + this.xMin = xMin; + this.yMin = yMin; + this.xMax = xMax; + this.yMax = yMax; + this.varIndexBase = varIndexBase; + } + + public override Bounds GetBounds(IVariationResolver? varResolver) + { + float dx0 = varResolver?.ResolveDelta(this.varIndexBase + 0u) ?? 0f; + float dy0 = varResolver?.ResolveDelta(this.varIndexBase + 1u) ?? 0f; + float dx1 = varResolver?.ResolveDelta(this.varIndexBase + 2u) ?? 0f; + float dy1 = varResolver?.ResolveDelta(this.varIndexBase + 3u) ?? 0f; + + float xMin = this.xMin + dx0; + float yMin = this.yMin + dy0; + float xMax = this.xMax + dx1; + float yMax = this.yMax + dy1; + + return new Bounds(xMin, yMin, xMax, yMax); + } +} diff --git a/src/SixLabors.Fonts/Tables/General/Colr/ClipList.cs b/src/SixLabors.Fonts/Tables/General/Colr/ClipList.cs new file mode 100644 index 000000000..019444590 --- /dev/null +++ b/src/SixLabors.Fonts/Tables/General/Colr/ClipList.cs @@ -0,0 +1,113 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics.CodeAnalysis; + +namespace SixLabors.Fonts.Tables.General.Colr; + +internal sealed class ClipList +{ + public ClipList(ClipRecord[] records, ClipBox?[] boxes) + { + this.Records = records; + this.Boxes = boxes; + } + + public ClipRecord[] Records { get; } + + // One ClipBox per record; null means "no box". + public ClipBox?[] Boxes { get; } + + public int Count => this.Records.Length; + + public static ClipList? Load(BigEndianBinaryReader reader, long offset) + { + if (offset == 0) + { + return null; + } + + reader.Seek(offset, SeekOrigin.Begin); + + _ = reader.ReadByte(); // Version. Always 1. + uint count = reader.ReadUInt32(); + + ClipRecord[] records = new ClipRecord[count]; + for (int i = 0; i < count; i++) + { + ushort start = reader.ReadUInt16(); + ushort end = reader.ReadUInt16(); + uint boxOffset = reader.ReadOffset24(); + records[i] = new ClipRecord(start, end, boxOffset); + } + + // TODO: Should this be nullable? + ClipBox?[] boxes = new ClipBox?[count]; + for (int i = 0; i < count; i++) + { + uint boxOffset = records[i].ClipBoxOffset; + reader.Seek(offset + boxOffset, SeekOrigin.Begin); + + byte format = reader.ReadByte(); + short xMin = reader.ReadFWORD(); + short yMin = reader.ReadFWORD(); + short xMax = reader.ReadFWORD(); + short yMax = reader.ReadFWORD(); + + switch (format) + { + case 1: + boxes[i] = new ClipBoxFormat1(xMin, yMin, xMax, yMax); + break; + + case 2: + uint varIndexBase = reader.ReadUInt32(); + boxes[i] = new ClipBoxFormat2(xMin, yMin, xMax, yMax, varIndexBase); + break; + + default: + boxes[i] = null; // Unknown format + break; + } + } + + return new ClipList(records, boxes); + } + + public bool TryGetClipBox(ushort glyphId, IVariationResolver? varResolver, [NotNullWhen(true)] out Bounds? bounds) + { + int lo = 0; + int hi = this.Records.Length - 1; + + while (lo <= hi) + { + int mid = (lo + hi) >> 1; + ClipRecord rec = this.Records[mid]; + + if (glyphId < rec.StartGlyphId) + { + hi = mid - 1; + continue; + } + + if (glyphId > rec.EndGlyphId) + { + lo = mid + 1; + continue; + } + + ClipBox? box = this.Boxes[mid]; + if (box is null) + { + bounds = null; + return false; + } + + bounds = box.GetBounds(varResolver); + return true; + } + + bounds = null; + return false; + } +} diff --git a/src/SixLabors.Fonts/Tables/General/Colr/ClipRecord.cs b/src/SixLabors.Fonts/Tables/General/Colr/ClipRecord.cs new file mode 100644 index 000000000..b61c2b927 --- /dev/null +++ b/src/SixLabors.Fonts/Tables/General/Colr/ClipRecord.cs @@ -0,0 +1,21 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables.General.Colr; + +internal readonly struct ClipRecord +{ + public ClipRecord(ushort startGlyphId, ushort endGlyphId, uint clipBoxOffset) + { + this.StartGlyphId = startGlyphId; + this.EndGlyphId = endGlyphId; + this.ClipBoxOffset = clipBoxOffset; + } + + public ushort StartGlyphId { get; } + + public ushort EndGlyphId { get; } + + // Offset (from start of COLR table) to a ClipBox (Format1/2) defining the clip region. + public uint ClipBoxOffset { get; } +} diff --git a/src/SixLabors.Fonts/Tables/General/Colr/ColorLine.cs b/src/SixLabors.Fonts/Tables/General/Colr/ColorLine.cs new file mode 100644 index 000000000..22f99f234 --- /dev/null +++ b/src/SixLabors.Fonts/Tables/General/Colr/ColorLine.cs @@ -0,0 +1,33 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables.General.Colr; + +internal sealed class ColorLine +{ + public ColorLine(Extend extend, ColorStop[] stops) + { + this.Extend = extend; + this.Stops = stops; + } + + public Extend Extend { get; } + + public ColorStop[] Stops { get; } + + public int Count => this.Stops.Length; + + public static ColorLine Load(BigEndianBinaryReader reader) + { + Extend extend = reader.ReadByte(); + ushort numStops = reader.ReadUInt16(); + + ColorStop[] stops = new ColorStop[numStops]; + for (int i = 0; i < numStops; i++) + { + stops[i] = ColorStop.Load(reader); + } + + return new ColorLine(extend, stops); + } +} diff --git a/src/SixLabors.Fonts/Tables/General/Colr/ColorStop.cs b/src/SixLabors.Fonts/Tables/General/Colr/ColorStop.cs new file mode 100644 index 000000000..abcfbb340 --- /dev/null +++ b/src/SixLabors.Fonts/Tables/General/Colr/ColorStop.cs @@ -0,0 +1,26 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; + +namespace SixLabors.Fonts.Tables.General.Colr; + +internal readonly struct ColorStop +{ + public ColorStop(float stopOffset, ushort paletteIndex, float alpha) + { + this.StopOffset = stopOffset; + this.PaletteIndex = paletteIndex; + this.Alpha = alpha; + } + + public float StopOffset { get; } + + public ushort PaletteIndex { get; } + + public float Alpha { get; } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ColorStop Load(BigEndianBinaryReader reader) + => new(reader.ReadF2Dot14(), reader.ReadUInt16(), reader.ReadF2Dot14()); +} diff --git a/src/SixLabors.Fonts/Tables/General/Colr/ColrCompositeMode.cs b/src/SixLabors.Fonts/Tables/General/Colr/ColrCompositeMode.cs new file mode 100644 index 000000000..09322cb1d --- /dev/null +++ b/src/SixLabors.Fonts/Tables/General/Colr/ColrCompositeMode.cs @@ -0,0 +1,40 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables.General.Colr; + +// Formats 32/33: PaintComposite / PaintVarComposite +internal enum ColrCompositeMode : byte +{ + // Porter-Duff modes + Clear = 0, + Src = 1, + Dst = 2, + SrcOver = 3, + DstOver = 4, + SrcIn = 5, + DstIn = 6, + SrcOut = 7, + DstOut = 8, + SrcAtop = 9, + DstAtop = 10, + Xor = 11, + Plus = 12, + + // Separable color blend modes: + Screen = 13, + Overlay = 14, + Darken = 15, + Lighten = 16, + ColorDodge = 17, + ColorBurn = 18, + HardLight = 19, + SoftLight = 20, + Difference = 21, + Exclusion = 22, + Multiply = 23, + Hue = 24, + Saturation = 25, + Color = 26, + Luminosity = 27 +} diff --git a/src/SixLabors.Fonts/Tables/General/Colr/ColrGlyphSourceBase.cs b/src/SixLabors.Fonts/Tables/General/Colr/ColrGlyphSourceBase.cs new file mode 100644 index 000000000..299bc6748 --- /dev/null +++ b/src/SixLabors.Fonts/Tables/General/Colr/ColrGlyphSourceBase.cs @@ -0,0 +1,401 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using SixLabors.Fonts.Rendering; +using SixLabors.Fonts.Tables.TrueType.Glyphs; + +namespace SixLabors.Fonts.Tables.General.Colr; + +/// +/// A base class for COLR glyph sources. +/// +internal abstract class ColrGlyphSourceBase : IPaintedGlyphSource +{ + /// + /// Initializes a new instance of the class. + /// + /// The COLR table. + /// The CPAL table, or null if not present. + /// Delegate that loads a glyph outline for the given glyph id. + public ColrGlyphSourceBase(ColrTable colr, CpalTable? cpal, Func glyphLoader) + { + this.Colr = colr; + this.Cpal = cpal; + this.GlyphLoader = glyphLoader; + } + + /// + /// Gets the COLR table. + /// + protected ColrTable Colr { get; } + + /// + /// Gets the CPAL table, or null if not present. + /// + protected CpalTable? Cpal { get; } + + /// + /// Gets the glyph loader delegate. + /// + protected Func GlyphLoader { get; } + + /// + public abstract bool TryGetPaintedGlyph(ushort glyphId, out PaintedGlyph glyph, out PaintedCanvasMetadata canvas); + + /// + /// Recursively flattens a COLR paint graph: + /// - Wrapper nodes pre-multiply their matrix into and recurse to the child. + /// - Composite emits backdrop subtree first (inherits ), + /// then source subtree with currentCompositeMode = node.CompositeMode. + /// - Leaf nodes emit concrete Rendering.Paint with Transform = accum and CompositeMode = currentBlend ?? default. + /// Colors and stop offsets are passed through; no Y-flip applied here. + /// + /// The COLR paint node. + /// The affine matrix in document space. + /// The active composite mode to apply to leaf paints, or null for default. + /// Optional CPAL palette for color resolution. + /// Collector for emitted leaf paints. + protected static void FlattenPaint( + Paint node, + Matrix3x2 transform, + CompositeMode mode, + CpalTable? cpal, + List outLeaves) + { + // The input not will only be a paintable leaf here, as upstream resolution + // should have eliminated glyph/colr-glyph nodes and flattened composites. + switch (node) + { + case PaintSolid ps: + { + if (ps.PaletteIndex == 0xFFFF) + { + // "Use foreground" => represent as a SolidPaint with fully transparent color; + // renderer can substitute foreground if needed. + outLeaves.Add(new SolidPaint + { + Color = new GlyphColor(0, 0, 0, 0), + Opacity = 1F, + Transform = transform, + CompositeMode = mode + }); + return; + } + + GlyphColor color = ResolveColor(cpal, ps.PaletteIndex, ps.Alpha); + outLeaves.Add(new SolidPaint + { + Color = color, + Opacity = 1F, + Transform = transform, + CompositeMode = mode + }); + return; + } + + case PaintLinearGradient pl: + { + GradientStop[] stops = ResolveStops(pl.ColorLine, cpal); + outLeaves.Add(new LinearGradientPaint + { + Units = GradientUnits.UserSpaceOnUse, + P0 = new Vector2(pl.X0, pl.Y0), + P1 = new Vector2(pl.X1, pl.Y1), + P2 = new Vector2(pl.X2, pl.Y2), + Spread = MapSpread(pl.ColorLine.Extend), + Stops = stops, + Opacity = 1F, + Transform = transform, + CompositeMode = mode + }); + return; + } + + case PaintVarLinearGradient vpl: + { + GradientStop[] stops = ResolveStops(vpl.ColorLine, cpal); + outLeaves.Add(new LinearGradientPaint + { + Units = GradientUnits.UserSpaceOnUse, + P0 = new Vector2(vpl.X0, vpl.Y0), + P1 = new Vector2(vpl.X1, vpl.Y1), + P2 = new Vector2(vpl.X2, vpl.Y2), + Spread = MapSpread(vpl.ColorLine.Extend), + Stops = stops, + Opacity = 1F, + Transform = transform, + CompositeMode = mode + }); + return; + } + + case PaintRadialGradient pr: + { + GradientStop[] stops = ResolveStops(pr.ColorLine, cpal); + outLeaves.Add(new RadialGradientPaint + { + Units = GradientUnits.UserSpaceOnUse, + Center0 = new Vector2(pr.X0, pr.Y0), + Radius0 = pr.Radius0, + Center1 = new Vector2(pr.X1, pr.Y1), + Radius1 = pr.Radius1, + Spread = MapSpread(pr.ColorLine.Extend), + Stops = stops, + Opacity = 1F, + Transform = transform, + CompositeMode = mode + }); + return; + } + + case PaintVarRadialGradient vpr: + { + GradientStop[] stops = ResolveStops(vpr.ColorLine, cpal); + outLeaves.Add(new RadialGradientPaint + { + Units = GradientUnits.UserSpaceOnUse, + Center0 = new Vector2(vpr.X0, vpr.Y0), + Radius0 = vpr.Radius0, + Center1 = new Vector2(vpr.X1, vpr.Y1), + Radius1 = vpr.Radius1, + Spread = MapSpread(vpr.ColorLine.Extend), + Stops = stops, + Opacity = 1F, + Transform = transform, + CompositeMode = mode + }); + return; + } + + case PaintSweepGradient sw: + { + GradientStop[] stops = ResolveStops(sw.ColorLine, cpal); + outLeaves.Add(new SweepGradientPaint + { + Units = GradientUnits.UserSpaceOnUse, + Center = new Vector2(sw.CenterX, sw.CenterY), + + // Spec says: add 1.0 and multiply by 180° to retrieve counter-clockwise degrees. + StartAngle = (sw.StartAngle + 1F) * 180F, + EndAngle = (sw.EndAngle + 1F) * 180F, + Spread = MapSpread(sw.ColorLine.Extend), + Stops = stops, + Opacity = 1F, + Transform = transform, + CompositeMode = mode + }); + return; + } + + case PaintVarSweepGradient vsw: + { + GradientStop[] stops = ResolveStops(vsw.ColorLine, cpal); + outLeaves.Add(new SweepGradientPaint + { + Units = GradientUnits.UserSpaceOnUse, + Center = new Vector2(vsw.CenterX, vsw.CenterY), + StartAngle = (vsw.StartAngle + 1F) * 180F, + EndAngle = (vsw.EndAngle + 1F) * 180F, + Spread = MapSpread(vsw.ColorLine.Extend), + Stops = stops, + Opacity = 1F, + Transform = transform, + CompositeMode = mode + }); + return; + } + + default: + return; + } + } + + /// + /// Converts a glyph vector into a sequence of path commands. + /// + /// The glyph vector. + protected static List BuildPath(GlyphVector gv) + { + IList points = gv.ControlPoints; + IReadOnlyList ends = gv.EndPoints; + + List cmds = new(points.Count + ends.Count); + + int endOfContour = -1; + + for (int ci = 0; ci < ends.Count; ci++) + { + int startOfContour = endOfContour + 1; + endOfContour = ends[ci]; + + if (endOfContour < startOfContour) + { + continue; + } + + int length = endOfContour - startOfContour + 1; + if (length == 0) + { + continue; + } + + // Choose initial MoveTo: last on-curve, else first on-curve, else midpoint(last, first). + ControlPoint first = points[startOfContour]; + ControlPoint last = points[endOfContour]; + + Vector2 moveTo = last.OnCurve ? last.Point + : first.OnCurve ? first.Point + : Mid(last.Point, first.Point); + + cmds.Add(PathCommand.MoveTo(moveTo)); + + // Ring traversal over input points. + Vector2 curr = last.Point; + Vector2 next = first.Point; + + for (int p = 0; p < length; p++) + { + Vector2 prev = curr; + curr = next; + + int currentIndex = startOfContour + p; + int nextIndex = startOfContour + ((p + 1) % length); + int prevIndex = startOfContour + ((length + p - 1) % length); + + next = points[nextIndex].Point; + + bool currOn = points[currentIndex].OnCurve; + bool prevOn = points[prevIndex].OnCurve; + bool nextOn = points[nextIndex].OnCurve; + + if (currOn) + { + // Emit line to the current on-curve point unconditionally. + cmds.Add(PathCommand.LineTo(curr)); + continue; + } + + // Off-curve: insert implicit on-curve midpoints. + Vector2 prev2 = prevOn ? prev : Mid(curr, prev); + Vector2 next2 = nextOn ? next : Mid(curr, next); + + if (!prevOn) + { + // Conditional line when previous input point was off-curve. + cmds.Add(PathCommand.LineTo(prev2)); + } + + // Metrics emits a LineTo(prev2) immediately before the quadratic as well. + cmds.Add(PathCommand.LineTo(prev2)); + + // Quadratic segment with control at current off-curve and endpoint at next2. + cmds.Add(PathCommand.QuadraticTo(curr, next2)); + } + + cmds.Add(PathCommand.Close()); + } + + return cmds; + } + + /// + /// Maps COLR Extend to renderer SpreadMethod. + /// + /// The COLR extend mode. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static SpreadMethod MapSpread(Extend extend) + => extend switch + { + Extend.Pad => SpreadMethod.Pad, + Extend.Repeat => SpreadMethod.Repeat, + Extend.Reflect => SpreadMethod.Reflect, + _ => SpreadMethod.Pad + }; + + /// + /// Resolves a color line into concrete gradient stops. Offsets are clamped to [0,1]. + /// 0xFFFF palette indices are treated as transparent here (foreground color handled by text color elsewhere). + /// + /// The color line. + /// The CPAL table, or null if not present. + /// The resolved gradient stops. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static GradientStop[] ResolveStops(ColorLine line, CpalTable? cpal) + { + ColorStop[] src = line.Stops; + GradientStop[] stops = new GradientStop[src.Length]; + + for (int i = 0; i < src.Length; i++) + { + ref readonly ColorStop s = ref src[i]; + + GlyphColor c = s.PaletteIndex == 0xFFFF + ? new GlyphColor(0, 0, 0, 0) // transparent placeholder; renderer can blend with foreground + : ResolveColor(cpal, s.PaletteIndex, s.Alpha); + + float offset = Math.Clamp(s.StopOffset, 0F, 1F); + + stops[i] = new GradientStop(offset, c); + } + + return stops; + } + + /// + /// Resolves a color line into concrete gradient stops. Offsets are clamped to [0,1]. + /// 0xFFFF palette indices are treated as transparent here (foreground color handled by text color elsewhere). + /// + /// The color line. + /// The CPAL table, or null if not present. + /// The resolved gradient stops. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static GradientStop[] ResolveStops(VarColorLine line, CpalTable? cpal) + { + VarColorStop[] src = line.Stops; + GradientStop[] stops = new GradientStop[src.Length]; + + for (int i = 0; i < src.Length; i++) + { + ref readonly VarColorStop s = ref src[i]; + + GlyphColor c = s.PaletteIndex == 0xFFFF + ? new GlyphColor(0, 0, 0, 0) // transparent placeholder; renderer can blend with foreground + : ResolveColor(cpal, s.PaletteIndex, s.Alpha); + + float offset = Math.Clamp(s.StopOffset, 0F, 1F); + + stops[i] = new GradientStop(offset, c); + } + + return stops; + } + + /// + /// Resolves a CPAL palette entry with an alpha multiplier. + /// + /// The CPAL table, or null if not present. + /// The palette entry index. + /// The alpha multiplier. + /// The resolved color. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static GlyphColor ResolveColor(CpalTable? cpal, int paletteEntryIndex, float alphaMul) + { + // Palette index 0 selection. If you later expose palette selection, thread it here. + GlyphColor baseColor = cpal is null ? new GlyphColor(0, 0, 0, 0) : cpal.GetGlyphColor(0, paletteEntryIndex); + + byte a = (byte)Math.Clamp((int)MathF.Round(baseColor.A * alphaMul), 0, 255); + return new GlyphColor(baseColor.R, baseColor.G, baseColor.B, a); + } + + /// + /// Calculates the midpoint between two vectors. + /// + /// The first vector to use in the midpoint calculation. + /// The second vector to use in the midpoint calculation. + /// A representing the point exactly halfway between the two input vectors. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector2 Mid(Vector2 a, Vector2 b) + => (a + b) * .5F; +} diff --git a/src/SixLabors.Fonts/Tables/General/Colr/ColrTable.cs b/src/SixLabors.Fonts/Tables/General/Colr/ColrTable.cs index 5bbabf12b..3ce87216a 100644 --- a/src/SixLabors.Fonts/Tables/General/Colr/ColrTable.cs +++ b/src/SixLabors.Fonts/Tables/General/Colr/ColrTable.cs @@ -1,20 +1,57 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Numerics; +using System.Runtime.CompilerServices; +using SixLabors.Fonts.Rendering; + namespace SixLabors.Fonts.Tables.General.Colr; internal class ColrTable : Table { internal const string TableName = "COLR"; + + // v0 private readonly BaseGlyphRecord[] glyphRecords; private readonly LayerRecord[] layers; - public ColrTable(BaseGlyphRecord[] glyphRecords, LayerRecord[] layers) + // v1 (nullable if not present) + private readonly BaseGlyphList? baseGlyphList; + private readonly LayerList? layerList; + private readonly ClipList? clipList; + + // Caches (offset -> resolved object) + private readonly Dictionary? paintCache; + + public ColrTable( + BaseGlyphRecord[] glyphRecords, + LayerRecord[] layers) + : this(glyphRecords, layers, null, null, null, null, 0) + { + } + + public ColrTable( + BaseGlyphRecord[] glyphRecords, + LayerRecord[] layers, + BaseGlyphList? baseGlyphList, + LayerList? layerList, + ClipList? clipList, + Dictionary? paintCache = null, + int version = 1) { this.glyphRecords = glyphRecords; this.layers = layers; + this.baseGlyphList = baseGlyphList; + this.layerList = layerList; + this.clipList = clipList; + this.paintCache = paintCache; + this.Version = version; } + public int Version { get; } + public static ColrTable? Load(FontReader fontReader) { if (!fontReader.TryGetReaderAtTablePosition(TableName, out BigEndianBinaryReader? binaryReader)) @@ -30,7 +67,7 @@ public ColrTable(BaseGlyphRecord[] glyphRecords, LayerRecord[] layers) internal Span GetLayers(ushort glyph) { - foreach (BaseGlyphRecord? g in this.glyphRecords) + foreach (BaseGlyphRecord g in this.glyphRecords) { if (g.GlyphId == glyph) { @@ -38,7 +75,512 @@ internal Span GetLayers(ushort glyph) } } - return Span.Empty; + return []; + } + + /// + /// Determines whether the specified glyph has an associated COLR v0 color glyph definition. + /// + /// The identifier of the glyph to check for a COLR v0 color glyph definition. + /// + /// if the specified glyph has a COLR v0 color glyph definition; otherwise, . + /// + public bool ContainsColorV0Glyph(ushort glyphId) + { + for (int i = 0; i < this.glyphRecords.Length; i++) + { + BaseGlyphRecord g = this.glyphRecords[i]; + if (g.GlyphId == glyphId) + { + return true; + } + } + + return false; + } + + /// + /// Determines whether the specified glyph has an associated COLR v1 color glyph definition. + /// + /// The identifier of the glyph to check for a COLR v1 color glyph definition. + /// + /// if the specified glyph has a COLR v1 color glyph definition; otherwise, . + /// + public bool ContainsColorV1Glyph(ushort glyphId) + { + if (this.baseGlyphList is null || this.layerList is null || this.paintCache is null) + { + return false; // No COLR v1 data + } + + return this.TryGetRootPaintOffset(glyphId, out uint _); + } + + /// + /// Attempts to retrieve the set of color layer records associated with the specified glyph. + /// + /// The glyph ID for which to retrieve color layer records. + /// + /// When this method returns, contains a span of structures + /// representing the color layers for the specified glyph, if found; otherwise, an empty span. + /// + /// + /// if color layer records are found for the specified glyph; otherwise, + /// . + /// + internal bool TryGetColrV0Layers(ushort glyph, out Span records) + { + for (int i = 0; i < this.glyphRecords.Length; i++) + { + BaseGlyphRecord g = this.glyphRecords[i]; + if (g.GlyphId == glyph) + { + records = this.layers.AsSpan().Slice(g.FirstLayerIndex, g.LayerCount); + return true; + } + } + + records = []; + return false; + } + + /// + /// Attempts to resolve and retrieve the list of color glyph layers for the specified glyph ID. + /// + /// The identifier of the glyph for which to resolve color layers. + /// + /// When this method returns, contains a list of resolved glyph layers if the operation succeeds; otherwise, + /// . This parameter is passed uninitialized. + /// + /// if the color glyph layers were successfully resolved; otherwise, . + /// + internal bool TryGetColrV1Layers(ushort glyphId, [NotNullWhen(true)] out List? layers) + { + layers = null; + + if (this.baseGlyphList is null || this.layerList is null || this.paintCache is null) + { + return false; // No COLR v1 data + } + + // 1) Resolve root paint for the requested base glyph + if (!this.TryGetRootPaintOffset(glyphId, out uint rootOff) || rootOff == 0) + { + return false; + } + + if (!this.paintCache.TryGetValue(rootOff, out Paint? root) || root is null) + { + return false; + } + + // 2) Flatten paint graph to layers. Start with no current glyph id. + List acc = []; + this.FlattenPaintToLayers(root, null, Matrix3x2.Identity, CompositeMode.SrcOver, acc); + + // 3) If nothing emitted, the graph did not bind any geometry (no PaintGlyph/ColrGlyph reached). + if (acc.Count == 0) + { + layers = null; + return false; + } + + layers = acc; + return true; + } + + /// + /// Recursively flattens a COLR v1 paint subtree into s. + /// A layer is emitted only when a leaf paint is reached under an active glyph-binding node: + /// + /// PaintGlyph sets the current glyph id to its GlyphId and recurses into its child paint. + /// PaintColrGlyph resolves that glyph's root paint, sets the current glyph id, and recurses. + /// Wrapper nodes (transform/translate/scale/rotate/skew, var forms) forward the current glyph id unchanged. + /// PaintComposite flattens both branches independently, forwarding the current glyph id to each. + /// Leaf paints (solid/linear/radial/sweep, var forms) emit a layer only if has a value. + /// + /// + /// The paint node to flatten. + /// + /// The glyph id whose outline will receive the paint. Set by PaintGlyph/PaintColrGlyph. + /// + /// Accumulated transform. + /// Accumulated composite mode. + /// Accumulator for resolved layers. + private void FlattenPaintToLayers( + Paint node, + ushort? currentGlyphId, + Matrix3x2 transform, + CompositeMode compositeMode, + List outLayers) + { + switch (node) + { + // --------------------------- + // Containers and indirections + // --------------------------- + case PaintColrLayers pcl: + { + // Iterates layer indices and flattens each addressed paint subtree. + // No glyph id is implied here; child subtrees must bind via PaintGlyph/ColrGlyph. + int first = (int)pcl.FirstLayerIndex; + int count = pcl.NumLayers; + ReadOnlySpan offs = this.GetLayerPaintOffsets(first, count); + + for (int i = 0; i < offs.Length; i++) + { + uint off = offs[i]; + if (off == 0) + { + continue; + } + + if (this.paintCache!.TryGetValue(off, out Paint? child) && child is not null) + { + this.FlattenPaintToLayers(child, currentGlyphId, transform, compositeMode, outLayers); + } + } + + return; + } + + case PaintColrGlyph pcg: + { + // Resolve the referenced glyph's root paint and recurse under that glyph id. + if (this.TryGetRootPaintOffset(pcg.GlyphId, out uint off) && off != 0 + && this.paintCache!.TryGetValue(off, out Paint? colrRoot) && colrRoot is not null) + { + this.FlattenPaintToLayers(colrRoot, pcg.GlyphId, transform, compositeMode, outLayers); + } + + return; + } + + case PaintGlyph pg: + { + // Bind geometry to the specified glyph id and recurse into its child paint. + this.FlattenPaintToLayers(pg.Child, pg.GlyphId, transform, compositeMode, outLayers); + return; + } + + // --------------------------- + // Wrappers: forward glyph id + // --------------------------- + case PaintTransform pt: + { + transform *= ToMatrix(pt.Transform); + this.FlattenPaintToLayers(pt.Child, currentGlyphId, transform, compositeMode, outLayers); + return; + } + + case PaintVarTransform pvt: + { + transform *= ToMatrix(pvt.Transform); + this.FlattenPaintToLayers(pvt.Child, currentGlyphId, transform, compositeMode, outLayers); + return; + } + + case PaintTranslate t: + { + transform *= Matrix3x2.CreateTranslation(t.Dx, t.Dy); + this.FlattenPaintToLayers(t.Child, currentGlyphId, transform, compositeMode, outLayers); + return; + } + + case PaintVarTranslate vt: + { + transform *= Matrix3x2.CreateTranslation(vt.Dx, vt.Dy); + this.FlattenPaintToLayers(vt.Child, currentGlyphId, transform, compositeMode, outLayers); + return; + } + + case PaintScale s: + { + transform *= BuildScale(s.ScaleX, s.ScaleY, s.AroundCenter, s.CenterX, s.CenterY); + this.FlattenPaintToLayers(s.Child, currentGlyphId, transform, compositeMode, outLayers); + return; + } + + case PaintVarScale vs: + { + transform *= BuildScale(vs.ScaleX, vs.ScaleY, vs.AroundCenter, vs.CenterX, vs.CenterY); + this.FlattenPaintToLayers(vs.Child, currentGlyphId, transform, compositeMode, outLayers); + return; + } + + case PaintRotate r: + { + transform *= BuildRotate(r.Angle, r.AroundCenter, r.CenterX, r.CenterY); + this.FlattenPaintToLayers(r.Child, currentGlyphId, transform, compositeMode, outLayers); + return; + } + + case PaintVarRotate vr: + { + transform *= BuildRotate(vr.Angle, vr.AroundCenter, vr.CenterX, vr.CenterY); + this.FlattenPaintToLayers(vr.Child, currentGlyphId, transform, compositeMode, outLayers); + return; + } + + case PaintSkew k: + { + transform *= BuildSkew(k.XSkew, k.YSkew, k.AroundCenter, k.CenterX, k.CenterY); + this.FlattenPaintToLayers(k.Child, currentGlyphId, transform, compositeMode, outLayers); + return; + } + + case PaintVarSkew vk: + { + transform *= BuildSkew(vk.XSkew, vk.YSkew, vk.AroundCenter, vk.CenterX, vk.CenterY); + this.FlattenPaintToLayers(vk.Child, currentGlyphId, transform, compositeMode, outLayers); + return; + } + + case PaintComposite comp: + { + compositeMode = MapCompositeMode(comp.CompositeMode); + + // Backdrop first, then Source. Both inherit the current glyph id. + this.FlattenPaintToLayers(comp.Backdrop, currentGlyphId, transform, compositeMode, outLayers); + this.FlattenPaintToLayers(comp.Source, currentGlyphId, transform, compositeMode, outLayers); + return; + } + + // --------------------------- + // Leaves: emit only if bound + // --------------------------- + case PaintSolid: + case PaintLinearGradient: + case PaintVarLinearGradient: + case PaintRadialGradient: + case PaintVarRadialGradient: + case PaintSweepGradient: + case PaintVarSweepGradient: + { + // Only emit if we have an active glyph id (i.e., we are inside a PaintGlyph/ColrGlyph branch). + if (currentGlyphId.HasValue) + { + _ = this.TryGetClipBox(currentGlyphId.Value, out Bounds? clip); + outLayers.Add(new ResolvedGlyphLayer(currentGlyphId.Value, node, transform, compositeMode, clip)); + } + + return; + } + + default: + { + // Unknown or unsupported node: do not emit and do not stop traversal. + return; + } + } + } + + /// + /// Maps an optional fixed 2×3 affine to . + /// Layout: + /// [ xx xy dx ] + /// [ yx yy dy ] + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Matrix3x2 ToMatrix(Affine2x3? affine) + { + if (affine.HasValue) + { + Affine2x3 a = affine.Value; + return new Matrix3x2(a.Xx, a.Yx, a.Xy, a.Yy, a.Dx, a.Dy); // (M11, M12, M21, M22, M31, M32) + } + + return Matrix3x2.Identity; + } + + /// + /// Maps an optional variable 2×3 affine to . + /// Layout: + /// [ xx xy dx ] + /// [ yx yy dy ] + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Matrix3x2 ToMatrix(VarAffine2x3? varAffine) + { + if (varAffine.HasValue) + { + VarAffine2x3 v = varAffine.Value; + return new Matrix3x2(v.Xx, v.Yx, v.Xy, v.Yy, v.Dx, v.Dy); + } + + return Matrix3x2.Identity; + } + + /// + /// Builds a scale matrix, optionally around a center. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Matrix3x2 BuildScale(float sx, float sy, bool aroundCenter, float cx, float cy) + { + if (!aroundCenter) + { + return Matrix3x2.CreateScale(sx, sy); + } + + return Matrix3x2.CreateScale(sx, sy, new Vector2(cx, cy)); + } + + /// + /// Builds a rotation matrix, optionally around a center. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Matrix3x2 BuildRotate(float angleColrUnits, bool aroundCenter, float cx, float cy) + { + // COLR: 1.0 == 180° => radians = angle * π + float radians = angleColrUnits * MathF.PI; + + if (!aroundCenter) + { + return Matrix3x2.CreateRotation(radians); + } + + return Matrix3x2.CreateRotation(radians, new Vector2(cx, cy)); + } + + /// + /// Builds a skew matrix, optionally around a center. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Matrix3x2 BuildSkew(float xSkew, float ySkew, bool aroundCenter, float cx, float cy) + { + // COLR: 1.0 == 180° => radians = angle * π + float rx = xSkew * MathF.PI; + float ry = ySkew * MathF.PI; + + if (!aroundCenter) + { + return Matrix3x2.CreateSkew(rx, ry); + } + + return Matrix3x2.CreateSkew(rx, ry, new Vector2(cx, cy)); + } + + /// + /// Maps a COLR composite mode to the internal . + /// + /// Returns when is null + /// or when the value is not recognized. + /// + /// + /// The optional COLR composite mode. + /// The mapped . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static CompositeMode MapCompositeMode(ColrCompositeMode? mode) + => mode switch + { + // Porter–Duff + ColrCompositeMode.Clear => CompositeMode.Clear, + ColrCompositeMode.Src => CompositeMode.Src, + ColrCompositeMode.Dst => CompositeMode.Dest, + ColrCompositeMode.SrcOver => CompositeMode.SrcOver, + ColrCompositeMode.DstOver => CompositeMode.DestOver, + ColrCompositeMode.SrcIn => CompositeMode.SrcIn, + ColrCompositeMode.DstIn => CompositeMode.DestIn, + ColrCompositeMode.SrcOut => CompositeMode.SrcOut, + ColrCompositeMode.DstOut => CompositeMode.DestOut, + ColrCompositeMode.SrcAtop => CompositeMode.SrcAtop, + ColrCompositeMode.DstAtop => CompositeMode.DestAtop, + ColrCompositeMode.Xor => CompositeMode.Xor, + ColrCompositeMode.Plus => CompositeMode.Plus, + + // Blend modes + ColrCompositeMode.Screen => CompositeMode.Screen, + ColrCompositeMode.Overlay => CompositeMode.Overlay, + ColrCompositeMode.Darken => CompositeMode.Darken, + ColrCompositeMode.Lighten => CompositeMode.Lighten, + ColrCompositeMode.ColorDodge => CompositeMode.ColorDodge, + ColrCompositeMode.ColorBurn => CompositeMode.ColorBurn, + ColrCompositeMode.HardLight => CompositeMode.HardLight, + ColrCompositeMode.SoftLight => CompositeMode.SoftLight, + ColrCompositeMode.Difference => CompositeMode.Difference, + ColrCompositeMode.Exclusion => CompositeMode.Exclusion, + ColrCompositeMode.Multiply => CompositeMode.Multiply, + ColrCompositeMode.Hue => CompositeMode.Hue, + ColrCompositeMode.Saturation => CompositeMode.Saturation, + ColrCompositeMode.Color => CompositeMode.Color, + ColrCompositeMode.Luminosity => CompositeMode.Luminosity, + _ => CompositeMode.SrcOver, + }; + + /// + /// Attempts to retrieve the paint table offset associated with the specified glyph ID. + /// + /// The glyph ID for which to look up the paint table offset. + /// + /// When this method returns, contains the paint table offset for the specified glyph ID, if found; otherwise, zero. + /// This parameter is passed uninitialized. + /// + /// + /// if the paint table offset was found for the specified glyph ID; otherwise, . + /// + private bool TryGetRootPaintOffset(ushort glyphId, out uint paintOffset) + { + if (this.baseGlyphList is null) + { + paintOffset = 0; + return false; + } + + ReadOnlySpan recs = this.baseGlyphList.Records; + int lo = 0, hi = recs.Length - 1; + + while (lo <= hi) + { + int mid = (lo + hi) >> 1; + ushort gid = recs[mid].GlyphId; + + if (glyphId == gid) + { + paintOffset = recs[mid].PaintOffset; + return true; + } + + if (glyphId < gid) + { + hi = mid - 1; + } + else + { + lo = mid + 1; + } + } + + paintOffset = 0; + return false; + } + + private ReadOnlySpan GetLayerPaintOffsets(int first, int count) + { + if (this.layerList is null || count <= 0) + { + return []; + } + + Span offsets = this.layerList.PaintOffsets.AsSpan(); + if ((uint)first >= (uint)offsets.Length) + { + return []; + } + + int len = Math.Min(count, offsets.Length - first); + return offsets.Slice(first, len); + } + + private bool TryGetClipBox(ushort glyphId, out Bounds? bounds) + { + if (this.clipList is null) + { + bounds = default; + return false; + } + + // TODO: support variation resolver + return this.clipList.TryGetClipBox(glyphId, null, out bounds); } public static ColrTable Load(BigEndianBinaryReader reader) @@ -52,49 +594,703 @@ public static ColrTable Load(BigEndianBinaryReader reader) // Offset32 | baseGlyphRecordsOffset | Offset(from beginning of COLR table) to Base Glyph records. // Offset32 | layerRecordsOffset | Offset(from beginning of COLR table) to Layer Records. // uint16 | numLayerRecords | Number of Layer Records. - - // Base Glyph Record - // Type | Name | Description - // ----------|------------------------|---------------------------------------------------------------------------------------------------- - // uint16 | gID | Glyph ID of reference glyph. This glyph is for reference only and is not rendered for color. - // uint16 | firstLayerIndex | Index(from beginning of the Layer Records) to the layer record. There will be numLayers consecutive entries for this base glyph. - // uint16 | numLayers | Number of color layers associated with this glyph. - - // Layer Record - // Type | Name | Description - // ----------|------------------------|---------------------------------------------------------------------------------------------------- - // uint16 | gID | Glyph ID of layer glyph (must be in z-order from bottom to top). - // uint16 | paletteIndex | Index value to use with a selected color palette. This value must be less than numPaletteEntries in - // | | > the CPAL table. A palette entry index value of 0xFFFF is a special case indicating that the text - // | | > foreground color (defined by a higher-level client) should be used and shall not be treated as - // | | > actual index into CPAL ColorRecord array. ushort version = reader.ReadUInt16(); ushort numBaseGlyphRecords = reader.ReadUInt16(); uint baseGlyphRecordsOffset = reader.ReadOffset32(); uint layerRecordsOffset = reader.ReadOffset32(); ushort numLayerRecords = reader.ReadUInt16(); - reader.Seek(baseGlyphRecordsOffset, System.IO.SeekOrigin.Begin); + uint baseGlyphListOffset = 0; + uint layerListOffset = 0; + uint clipListOffset = 0; + uint varIndexMapOffset = 0; + uint itemVariationStoreOffset = 0; + + if (version == 1) + { + // | Type | Name | Description | + // |----------|--------------------------|-------------------------------------------------------------------------------| + // | uint16 | version | Table version number—set to 1. | + // | uint16 | numBaseGlyphRecords | Number of BaseGlyph records; may be 0 in a version 1 table. | + // | Offset32 | baseGlyphRecordsOffset | Offset to baseGlyphRecords array, from beginning of COLR table (may be NULL). | + // | Offset32 | layerRecordsOffset | Offset to layerRecords array, from beginning of COLR table (may be NULL). | + // | uint16 | numLayerRecords | Number of Layer records; may be 0 in a version 1 table. | + // | Offset32 | baseGlyphListOffset | Offset to BaseGlyphList table, from beginning of COLR table. | + // | Offset32 | layerListOffset | Offset to LayerList table, from beginning of COLR table (may be NULL). | + // | Offset32 | clipListOffset | Offset to ClipList table, from beginning of COLR table (may be NULL). | + // | Offset32 | varIndexMapOffset | Offset to DeltaSetIndexMap table, from beginning of COLR table (may be NULL). | + // | Offset32 | itemVariationStoreOffset | Offset to ItemVariationStore, from beginning of COLR table (may be NULL). | + baseGlyphListOffset = reader.ReadOffset32(); + layerListOffset = reader.ReadOffset32(); + clipListOffset = reader.ReadOffset32(); + varIndexMapOffset = reader.ReadOffset32(); + itemVariationStoreOffset = reader.ReadOffset32(); + } + + // v0: BaseGlyph and Layer records (optional in v1; may be zero) + BaseGlyphRecord[] glyphs = []; + if (numBaseGlyphRecords != 0 && baseGlyphRecordsOffset != 0) + { + glyphs = new BaseGlyphRecord[numBaseGlyphRecords]; + reader.Seek(baseGlyphRecordsOffset, SeekOrigin.Begin); + + for (int i = 0; i < numBaseGlyphRecords; i++) + { + ushort gi = reader.ReadUInt16(); + ushort idx = reader.ReadUInt16(); + ushort num = reader.ReadUInt16(); + glyphs[i] = new BaseGlyphRecord(gi, idx, num); + } + } - var glyphs = new BaseGlyphRecord[numBaseGlyphRecords]; - var layers = new LayerRecord[numLayerRecords]; - for (int i = 0; i < numBaseGlyphRecords; i++) + LayerRecord[] layerRecs = []; + if (numLayerRecords != 0 && layerRecordsOffset != 0) { - ushort gi = reader.ReadUInt16(); - ushort idx = reader.ReadUInt16(); - ushort num = reader.ReadUInt16(); - glyphs[i] = new BaseGlyphRecord(gi, idx, num); + layerRecs = new LayerRecord[numLayerRecords]; + reader.Seek(layerRecordsOffset, SeekOrigin.Begin); + + for (int i = 0; i < numLayerRecords; i++) + { + ushort gi = reader.ReadUInt16(); + ushort pi = reader.ReadUInt16(); + layerRecs[i] = new LayerRecord(gi, pi); + } + } + + // v1: BaseGlyphList, LayerList, ClipList (nullable if not present) + BaseGlyphList? baseGlyphList = null; + LayerList? layerList = null; + ClipList? clipList = null; + Dictionary? paintCache = null; + + if (version == 1) + { + baseGlyphList = BaseGlyphList.Load(reader, baseGlyphListOffset); + layerList = LayerList.Load(reader, layerListOffset); + clipList = ClipList.Load(reader, clipListOffset); + + // varIndexMapOffset / itemVariationStoreOffset are parsed elsewhere if/when needed. + _ = varIndexMapOffset; + _ = itemVariationStoreOffset; + + paintCache = LoadPaintRoots(reader, baseGlyphList, layerList); } - reader.Seek(layerRecordsOffset, System.IO.SeekOrigin.Begin); + return new ColrTable(glyphs, layerRecs, baseGlyphList, layerList, clipList, paintCache, 1); + } + + private static Dictionary LoadPaintRoots( + BigEndianBinaryReader reader, + BaseGlyphList? baseGlyphList, + LayerList? layerList) + { + PaintCaches caches = new(); - for (int i = 0; i < numLayerRecords; i++) + // 1) Root paints from BaseGlyphList + if (baseGlyphList is not null) { - ushort gi = reader.ReadUInt16(); - ushort pi = reader.ReadUInt16(); - layers[i] = new LayerRecord(gi, pi); + foreach (BaseGlyphPaintRecord rec in baseGlyphList.Records) + { + if (rec.PaintOffset != 0) + { + _ = LoadPaintAt(reader, rec.PaintOffset, layerList, caches); + } + } + } + + // 2) All paints referenced by LayerList (PaintColrLayers points into these) + if (layerList is not null) + { + foreach (uint offset in layerList.PaintOffsets) + { + if (offset != 0) + { + _ = LoadPaintAt(reader, offset, layerList, caches); + } + } } - return new ColrTable(glyphs, layers); + return caches.PaintCache; } + + private static Paint LoadPaintAt( + BigEndianBinaryReader reader, + uint paintOffset, + LayerList? layerList, + PaintCaches caches) + { + if (caches.PaintCache.TryGetValue(paintOffset, out Paint? p)) + { + return p; + } + + long restore = reader.BaseStream.Position; + reader.Seek(paintOffset, SeekOrigin.Begin); + + byte format = reader.ReadByte(); + Paint result; + + switch (format) + { + // 1: PaintColrLayers + case 1: + { + byte numLayers = reader.ReadByte(); + uint firstLayerIndex = reader.ReadUInt32(); + + result = new PaintColrLayers + { + Format = format, + NumLayers = numLayers, + FirstLayerIndex = firstLayerIndex + }; + + // Walk children immediately: + if (layerList is not null) + { + for (uint i = 0; i < numLayers; i++) + { + int idx = (int)(firstLayerIndex + i); + uint layerPaintOff = layerList.PaintOffsets[idx]; + if (layerPaintOff != 0) + { + _ = LoadPaintAt(reader, layerPaintOff, layerList, caches); + } + } + } + + break; + } + + // 2/3: PaintSolid / PaintVarSolid + case 2: + { + ushort paletteIndex = reader.ReadUInt16(); + float alpha = reader.ReadF2Dot14(); + result = new PaintSolid { Format = format, PaletteIndex = paletteIndex, Alpha = alpha }; + break; + } + + case 3: + { + ushort paletteIndex = reader.ReadUInt16(); + float alpha = reader.ReadF2Dot14(); + uint varBase = reader.ReadUInt32(); + result = new PaintVarSolid { Format = format, PaletteIndex = paletteIndex, Alpha = alpha, VarIndexBase = varBase }; + break; + } + + // 4/5: PaintLinearGradient / PaintVarLinearGradient + case 4: + { + uint colorLineOff = reader.ReadOffset24(); + ColorLine line = LoadColorLineAt(reader, paintOffset + colorLineOff, caches); + short x0 = reader.ReadFWORD(); + short y0 = reader.ReadFWORD(); + short x1 = reader.ReadFWORD(); + short y1 = reader.ReadFWORD(); + short x2 = reader.ReadFWORD(); + short y2 = reader.ReadFWORD(); + result = new PaintLinearGradient { Format = format, ColorLine = line, X0 = x0, Y0 = y0, X1 = x1, Y1 = y1, X2 = x2, Y2 = y2 }; + break; + } + + case 5: + { + uint colorLineOff = reader.ReadOffset24(); + VarColorLine line = LoadVarColorLineAt(reader, paintOffset + colorLineOff, caches); + short x0 = reader.ReadFWORD(); + short y0 = reader.ReadFWORD(); + short x1 = reader.ReadFWORD(); + short y1 = reader.ReadFWORD(); + short x2 = reader.ReadFWORD(); + short y2 = reader.ReadFWORD(); + uint varBase = reader.ReadUInt32(); + result = new PaintVarLinearGradient + { + Format = format, + ColorLine = line, + X0 = x0, + Y0 = y0, + X1 = x1, + Y1 = y1, + X2 = x2, + Y2 = y2, + VarIndexBase = varBase + }; + break; + } + + // 6/7: PaintRadialGradient / PaintVarRadialGradient + case 6: + { + uint colorLineOff = reader.ReadOffset24(); + ColorLine line = LoadColorLineAt(reader, paintOffset + colorLineOff, caches); + short x0 = reader.ReadFWORD(); + short y0 = reader.ReadFWORD(); + ushort r0 = reader.ReadUFWORD(); + short x1 = reader.ReadFWORD(); + short y1 = reader.ReadFWORD(); + ushort r1 = reader.ReadUFWORD(); + result = new PaintRadialGradient + { + Format = format, + ColorLine = line, + X0 = x0, + Y0 = y0, + Radius0 = r0, + X1 = x1, + Y1 = y1, + Radius1 = r1 + }; + break; + } + + case 7: + { + uint colorLineOff = reader.ReadOffset24(); + VarColorLine line = LoadVarColorLineAt(reader, paintOffset + colorLineOff, caches); + short x0 = reader.ReadFWORD(); + short y0 = reader.ReadFWORD(); + ushort r0 = reader.ReadUFWORD(); + short x1 = reader.ReadFWORD(); + short y1 = reader.ReadFWORD(); + ushort r1 = reader.ReadUFWORD(); + uint varBase = reader.ReadUInt32(); + result = new PaintVarRadialGradient + { + Format = format, + ColorLine = line, + X0 = x0, + Y0 = y0, + Radius0 = r0, + X1 = x1, + Y1 = y1, + Radius1 = r1, + VarIndexBase = varBase + }; + break; + } + + // 8/9: PaintSweepGradient / PaintVarSweepGradient + case 8: + { + uint colorLineOff = reader.ReadOffset24(); + ColorLine line = LoadColorLineAt(reader, paintOffset + colorLineOff, caches); + short cx = reader.ReadFWORD(); + short cy = reader.ReadFWORD(); + float start = reader.ReadF2Dot14(); + float end = reader.ReadF2Dot14(); + result = new PaintSweepGradient + { + Format = format, + ColorLine = line, + CenterX = cx, + CenterY = cy, + StartAngle = start, + EndAngle = end + }; + break; + } + + case 9: + { + uint colorLineOff = reader.ReadOffset24(); + VarColorLine line = LoadVarColorLineAt(reader, paintOffset + colorLineOff, caches); + short cx = reader.ReadFWORD(); + short cy = reader.ReadFWORD(); + float start = reader.ReadF2Dot14(); + float end = reader.ReadF2Dot14(); + uint varBase = reader.ReadUInt32(); + result = new PaintVarSweepGradient + { + Format = format, + ColorLine = line, + CenterX = cx, + CenterY = cy, + StartAngle = start, + EndAngle = end, + VarIndexBase = varBase + }; + break; + } + + // 10: PaintGlyph + case 10: + { + uint childOff = reader.ReadOffset24(); + ushort gid = reader.ReadUInt16(); + Paint child = LoadPaintAt(reader, paintOffset + childOff, layerList, caches); + result = new PaintGlyph { Format = format, Child = child, GlyphId = gid }; + break; + } + + // 11: PaintColrGlyph + case 11: + { + ushort gid = reader.ReadUInt16(); + result = new PaintColrGlyph { Format = format, GlyphId = gid }; + + // Note: resolution of gid->root paint happens elsewhere when you interpret. + break; + } + + // 12/13: PaintTransform / PaintVarTransform + case 12: + { + uint childOff = reader.ReadOffset24(); + uint transformOff = reader.ReadOffset24(); + + Affine2x3 m = ReadAffine2x3At(reader, paintOffset + transformOff, caches); + Paint child = LoadPaintAt(reader, paintOffset + childOff, layerList, caches); + result = new PaintTransform { Format = format, Child = child, Transform = m }; + break; + } + + case 13: + { + uint childOff = reader.ReadOffset24(); + uint transformOff = reader.ReadOffset24(); + + VarAffine2x3 vm = ReadVarAffine2x3At(reader, paintOffset + transformOff, caches); + Paint child = LoadPaintAt(reader, paintOffset + childOff, layerList, caches); + result = new PaintVarTransform { Format = format, Child = child, Transform = vm }; + break; + } + + // 14/15: PaintTranslate / PaintVarTranslate + case 14: + { + uint childOff = reader.ReadOffset24(); + short dx = reader.ReadFWORD(); + short dy = reader.ReadFWORD(); + Paint child = LoadPaintAt(reader, paintOffset + childOff, layerList, caches); + result = new PaintTranslate { Format = format, Child = child, Dx = dx, Dy = dy }; + break; + } + + case 15: + { + uint childOff = reader.ReadOffset24(); + short dx = reader.ReadFWORD(); + short dy = reader.ReadFWORD(); + uint varBase = reader.ReadUInt32(); + Paint child = LoadPaintAt(reader, paintOffset + childOff, layerList, caches); + result = new PaintVarTranslate { Format = format, Child = child, Dx = dx, Dy = dy, VarIndexBase = varBase }; + break; + } + + // 16/17/18/19/20/21/22/23: Scale variants + case 16: // PaintScale + case 17: // PaintVarScale + case 18: // PaintScaleAroundCenter + case 19: // PaintVarScaleAroundCenter + case 20: // PaintScaleUniform + case 21: // PaintVarScaleUniform + case 22: // PaintScaleUniformAroundCenter + case 23: // PaintVarScaleUniformAroundCenter + { + bool aroundCenter = format is 18 or 19 or 22 or 23; + bool uniform = format is 20 or 21 or 22 or 23; + bool isVar = (format % 2) == 1; + + uint childOff = reader.ReadOffset24(); + float sx = reader.ReadF2Dot14(); + float sy = uniform ? sx : reader.ReadF2Dot14(); + + short cx = 0, cy = 0; + if (aroundCenter) + { + cx = reader.ReadFWORD(); + cy = reader.ReadFWORD(); + } + + uint varBase = isVar ? reader.ReadUInt32() : 0; + Paint child = LoadPaintAt(reader, paintOffset + childOff, layerList, caches); + + if (isVar) + { + result = new PaintVarScale + { + Format = format, + Child = child, + ScaleX = sx, + ScaleY = sy, + CenterX = cx, + CenterY = cy, + AroundCenter = aroundCenter, + Uniform = uniform, + VarIndexBase = varBase + }; + } + else + { + result = new PaintScale + { + Format = format, + Child = child, + ScaleX = sx, + ScaleY = sy, + CenterX = cx, + CenterY = cy, + AroundCenter = aroundCenter, + Uniform = uniform + }; + } + + break; + } + + // 24/25/26/27: Rotate variants + case 24: // PaintRotate + case 25: // PaintVarRotate + case 26: // PaintRotateAroundCenter + case 27: // PaintVarRotateAroundCenter + { + bool aroundCenter = format is 26 or 27; + bool isVar = (format % 2) == 1; + + uint childOff = reader.ReadOffset24(); + float angle = reader.ReadF2Dot14(); + + short cx = 0, cy = 0; + if (aroundCenter) + { + cx = reader.ReadFWORD(); + cy = reader.ReadFWORD(); + } + + uint varBase = isVar ? reader.ReadUInt32() : 0; + Paint child = LoadPaintAt(reader, paintOffset + childOff, layerList, caches); + + if (isVar) + { + result = new PaintVarRotate + { + Format = format, + Child = child, + Angle = angle, + CenterX = cx, + CenterY = cy, + AroundCenter = aroundCenter, + VarIndexBase = varBase + }; + } + else + { + result = new PaintRotate + { + Format = format, + Child = child, + Angle = angle, + CenterX = cx, + CenterY = cy, + AroundCenter = aroundCenter + }; + } + + break; + } + + // 28/29/30/31: Skew variants + case 28: // PaintSkew + case 29: // PaintVarSkew + case 30: // PaintSkewAroundCenter + case 31: // PaintVarSkewAroundCenter + { + bool aroundCenter = format is 30 or 31; + bool isVar = (format % 2) == 1; + + uint childOff = reader.ReadOffset24(); + float xskew = reader.ReadF2Dot14(); + float yskew = reader.ReadF2Dot14(); + + short cx = 0, cy = 0; + if (aroundCenter) + { + cx = reader.ReadFWORD(); + cy = reader.ReadFWORD(); + } + + uint varBase = isVar ? reader.ReadUInt32() : 0; + Paint child = LoadPaintAt(reader, paintOffset + childOff, layerList, caches); + + if (isVar) + { + result = new PaintVarSkew + { + Format = format, + Child = child, + XSkew = xskew, + YSkew = yskew, + CenterX = cx, + CenterY = cy, + AroundCenter = aroundCenter, + VarIndexBase = varBase + }; + } + else + { + result = new PaintSkew + { + Format = format, + Child = child, + XSkew = xskew, + YSkew = yskew, + CenterX = cx, + CenterY = cy, + AroundCenter = aroundCenter + }; + } + + break; + } + + // 32: Composite + case 32: + { + uint srcOff = reader.ReadOffset24(); + ColrCompositeMode mode = reader.ReadByte(); + uint backOff = reader.ReadOffset24(); + + Paint src = LoadPaintAt(reader, paintOffset + srcOff, layerList, caches); + Paint back = LoadPaintAt(reader, paintOffset + backOff, layerList, caches); + result = new PaintComposite { Format = format, CompositeMode = mode, Source = src, Backdrop = back }; + break; + } + + default: + // Unknown format -> treat as no-op solid (or throw). We'll store a stub. + result = new PaintSolid { Format = format, PaletteIndex = 0, Alpha = 0 }; + break; + } + + caches.PaintCache[paintOffset] = result; + reader.BaseStream.Position = restore; + return result; + } + + // Eager ColorLine loaders (offset-based) + private static ColorLine LoadColorLineAt(BigEndianBinaryReader reader, uint offset, PaintCaches caches) + { + if (caches.ColorLineCache.TryGetValue(offset, out ColorLine? line)) + { + return line; + } + + long restore = reader.BaseStream.Position; + reader.Seek(offset, SeekOrigin.Begin); + + line = ColorLine.Load(reader); + caches.ColorLineCache[offset] = line; + + reader.BaseStream.Position = restore; + + return line; + } + + private static VarColorLine LoadVarColorLineAt(BigEndianBinaryReader reader, uint offset, PaintCaches caches) + { + if (caches.VarColorLineCache.TryGetValue(offset, out VarColorLine? line)) + { + return line; + } + + long restore = reader.BaseStream.Position; + reader.Seek(offset, SeekOrigin.Begin); + + line = VarColorLine.Load(reader); + caches.VarColorLineCache[offset] = line; + + reader.BaseStream.Position = restore; + return line; + } + + // Affine readers (Fixed 16.16) + private static Affine2x3 ReadAffine2x3At(BigEndianBinaryReader reader, uint offset, PaintCaches caches) + { + if (caches.AffineCache.TryGetValue(offset, out Affine2x3 m)) + { + return m; + } + + long restore = reader.BaseStream.Position; + reader.Seek(offset, SeekOrigin.Begin); + + float xx = reader.ReadFixed(); + float yx = reader.ReadFixed(); + float xy = reader.ReadFixed(); + float yy = reader.ReadFixed(); + float dx = reader.ReadFixed(); + float dy = reader.ReadFixed(); + + m = new Affine2x3(xx, yx, xy, yy, dx, dy); + caches.AffineCache[offset] = m; + + reader.BaseStream.Position = restore; + return m; + } + + private static VarAffine2x3 ReadVarAffine2x3At(BigEndianBinaryReader reader, uint offset, PaintCaches caches) + { + if (caches.VarAffineCache.TryGetValue(offset, out VarAffine2x3 m)) + { + return m; + } + + long restore = reader.BaseStream.Position; + reader.Seek(offset, SeekOrigin.Begin); + + float xx = reader.ReadFixed(); + float yx = reader.ReadFixed(); + float xy = reader.ReadFixed(); + float yy = reader.ReadFixed(); + float dx = reader.ReadFixed(); + float dy = reader.ReadFixed(); + uint varBase = reader.ReadUInt32(); + + m = new VarAffine2x3(xx, yx, xy, yy, dx, dy, varBase); + caches.VarAffineCache[offset] = m; + + reader.BaseStream.Position = restore; + return m; + } +} + +internal sealed class PaintCaches +{ + public Dictionary PaintCache { get; } = []; + + public Dictionary ColorLineCache { get; } = []; + + public Dictionary VarColorLineCache { get; } = []; + + public Dictionary AffineCache { get; } = []; + + public Dictionary VarAffineCache { get; } = []; +} + +#pragma warning disable SA1201 // Elements should appear in the correct order +[DebuggerDisplay("Id: {GlyphId}")] +internal readonly struct ResolvedGlyphLayer +#pragma warning restore SA1201 // Elements should appear in the correct order +{ + public ResolvedGlyphLayer(ushort id, Paint paint, Matrix3x2 transform, CompositeMode mode, Bounds? clipBox) + { + this.GlyphId = id; + this.Paint = paint; + this.Transform = transform; + this.CompositeMode = mode; + this.ClipBox = clipBox; + } + + public ushort GlyphId { get; } + + public Paint Paint { get; } + + public Matrix3x2 Transform { get; } + + public CompositeMode CompositeMode { get; } + + public Bounds? ClipBox { get; } } diff --git a/src/SixLabors.Fonts/Tables/General/Colr/ColrV0GlyphSource.cs b/src/SixLabors.Fonts/Tables/General/Colr/ColrV0GlyphSource.cs new file mode 100644 index 000000000..3e64261c6 --- /dev/null +++ b/src/SixLabors.Fonts/Tables/General/Colr/ColrV0GlyphSource.cs @@ -0,0 +1,80 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Collections.Concurrent; +using System.Numerics; +using SixLabors.Fonts.Rendering; +using SixLabors.Fonts.Tables.TrueType.Glyphs; + +namespace SixLabors.Fonts.Tables.General.Colr; + +/// +/// Supplies painted glyphs for COLR v0 fonts. +/// Flattens paint graphs into a linear stream and emits a . +/// +internal sealed class ColrV0GlyphSource : ColrGlyphSourceBase +{ + private static readonly ConcurrentDictionary CachedGlyphs = []; + + /// + /// Initializes a new instance of the class. + /// + /// The COLR table. + /// The CPAL table, or null if not present. + /// Delegate that loads a glyph outline for the given glyph id. + public ColrV0GlyphSource(ColrTable colr, CpalTable? cpal, Func glyphLoader) + : base(colr, cpal, glyphLoader) + { + } + + /// + public override bool TryGetPaintedGlyph(ushort glyphId, out PaintedGlyph glyph, out PaintedCanvasMetadata canvas) + { + (PaintedGlyph Glyph, PaintedCanvasMetadata Canvas) result = CachedGlyphs.GetOrAdd(glyphId, id => + { + if (this.Colr.TryGetColrV0Layers(id, out Span resolved)) + { + List layers = new(resolved.Length); + for (int i = 0; i < resolved.Length; i++) + { + LayerRecord rl = resolved[i]; + GlyphVector? gv = this.GlyphLoader(rl.GlyphId); + if (gv is null || !gv.Value.HasValue()) + { + continue; + } + + // Build geometry once for this layer. + List path = BuildPath(gv.Value); + + // Flatten paint graph: attach composite mode to leaves. + List leafPaints = []; + PaintSolid paint = new() { PaletteIndex = rl.PaletteIndex, Alpha = 1, Format = 2 }; + FlattenPaint(paint, Matrix3x2.Identity, CompositeMode.SrcOver, this.Cpal, leafPaints); + + // Emit one layer per leaf paint. + for (int p = 0; p < leafPaints.Count; p++) + { + // Unlike COLR v1, COLR v0 leaves have no transform so we can reuse the same path. + Rendering.Paint leaf = leafPaints[p]; + layers.Add(new PaintedLayer(leaf, FillRule.NonZero, leaf.Transform, null, path)); + } + } + + if (layers.Count > 0) + { + // Canvas viewBox in Y-up; renderer downstream decides orientation via flag. + PaintedGlyph glyph = new(layers); + PaintedCanvasMetadata canvas = new(FontRectangle.Empty, isYDown: false, rootTransform: Matrix3x2.Identity); + return (glyph, canvas); + } + } + + return (default, default); + }); + + glyph = result.Glyph; + canvas = result.Canvas; + return result.Glyph.Layers.Count > 0; + } +} diff --git a/src/SixLabors.Fonts/Tables/General/Colr/ColrV1GlyphSource.cs b/src/SixLabors.Fonts/Tables/General/Colr/ColrV1GlyphSource.cs new file mode 100644 index 000000000..385c7ea20 --- /dev/null +++ b/src/SixLabors.Fonts/Tables/General/Colr/ColrV1GlyphSource.cs @@ -0,0 +1,89 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Collections.Concurrent; +using System.Numerics; +using SixLabors.Fonts.Rendering; +using SixLabors.Fonts.Tables.TrueType.Glyphs; + +namespace SixLabors.Fonts.Tables.General.Colr; + +/// +/// Supplies painted glyphs for COLR v1 fonts. +/// Flattens paint graphs into a linear stream and emits a . +/// +internal sealed class ColrV1GlyphSource : ColrGlyphSourceBase +{ + private static readonly ConcurrentDictionary CachedGlyphs = []; + + /// + /// Initializes a new instance of the class. + /// + /// The COLR table. + /// The CPAL table, or null if not present. + /// Delegate that loads a glyph outline for the given glyph id. + public ColrV1GlyphSource(ColrTable colr, CpalTable? cpal, Func glyphLoader) + : base(colr, cpal, glyphLoader) + { + } + + /// + public override bool TryGetPaintedGlyph(ushort glyphId, out PaintedGlyph glyph, out PaintedCanvasMetadata canvas) + { + (PaintedGlyph Glyph, PaintedCanvasMetadata Canvas) result = CachedGlyphs.GetOrAdd(glyphId, _ => + { + if (this.Colr.TryGetColrV1Layers(glyphId, out List? resolved)) + { + List layers = new(resolved.Count); + for (int i = 0; i < resolved.Count; i++) + { + ResolvedGlyphLayer rl = resolved[i]; + GlyphVector? gv = this.GlyphLoader(rl.GlyphId); + if (gv is null || !gv.Value.HasValue()) + { + continue; + } + + // Build geometry once for this layer. + List path = BuildPath(gv.Value); + + // Flatten paint graph: accumulate wrapper transforms; attach composite mode to leaves. + List leafPaints = []; + FlattenPaint(rl.Paint, rl.Transform, rl.CompositeMode, this.Cpal, leafPaints); + + // Emit one layer per leaf paint. + Bounds? clip = rl.ClipBox; + for (int p = 0; p < leafPaints.Count; p++) + { + Rendering.Paint leaf = leafPaints[p]; + Matrix3x2 xForm = Matrix3x2.Identity; + if (leaf is SolidPaint solid) + { + // Move the transform from the paint to the layer. + // We do this so that solid paints are also transformed correctly as + // their location is defined in the local space of the layer. + xForm = solid.Transform; + solid.Transform = Matrix3x2.Identity; + } + + layers.Add(new PaintedLayer(leaf, FillRule.NonZero, xForm, clip, path)); + } + } + + if (layers.Count > 0) + { + // Canvas viewBox in Y-up; renderer downstream decides orientation via flag. + PaintedGlyph glyph = new(layers); + PaintedCanvasMetadata canvas = new(FontRectangle.Empty, isYDown: false, rootTransform: Matrix3x2.Identity); + return (glyph, canvas); + } + } + + return (default, default); + }); + + glyph = result.Glyph; + canvas = result.Canvas; + return result.Glyph.Layers.Count > 0; + } +} diff --git a/src/SixLabors.Fonts/Tables/General/Colr/Extend.cs b/src/SixLabors.Fonts/Tables/General/Colr/Extend.cs new file mode 100644 index 000000000..38879b81d --- /dev/null +++ b/src/SixLabors.Fonts/Tables/General/Colr/Extend.cs @@ -0,0 +1,11 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables.General.Colr; + +internal enum Extend : byte +{ + Pad = 0, + Repeat = 1, + Reflect = 2 +} diff --git a/src/SixLabors.Fonts/Tables/General/Colr/IVariationResolver.cs b/src/SixLabors.Fonts/Tables/General/Colr/IVariationResolver.cs new file mode 100644 index 000000000..7abdb679d --- /dev/null +++ b/src/SixLabors.Fonts/Tables/General/Colr/IVariationResolver.cs @@ -0,0 +1,18 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +#pragma warning disable SA1201 // Elements should appear in the correct order +namespace SixLabors.Fonts.Tables.General.Colr; + +/// +/// Provides a mechanism to resolve variation index deltas. +/// +internal interface IVariationResolver +{ + /// + /// Calculates the resolved delta value for the specified variable index. + /// + /// The zero-based index of the variable for which to resolve the delta value. + /// The resolved delta value as a floating-point number for the specified variable index. + public float ResolveDelta(uint varIndex); +} diff --git a/src/SixLabors.Fonts/Tables/General/Colr/LayerList.cs b/src/SixLabors.Fonts/Tables/General/Colr/LayerList.cs new file mode 100644 index 000000000..8494b8f97 --- /dev/null +++ b/src/SixLabors.Fonts/Tables/General/Colr/LayerList.cs @@ -0,0 +1,39 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables.General.Colr; + +internal sealed class LayerList +{ + public LayerList(uint[] paintOffsets) + => this.PaintOffsets = paintOffsets; + + public uint[] PaintOffsets { get; } + + public int Count => this.PaintOffsets.Length; + + public static LayerList? Load(BigEndianBinaryReader reader, uint offset) + { + if (offset == 0) + { + return null; + } + + reader.Seek(offset, SeekOrigin.Begin); + uint count = reader.ReadUInt32(); + + if (count == 0) + { + return null; + } + + // Offsets are relative to the table start; convert to COLR-relative. + uint[] offsets = new uint[count]; + for (int i = 0; i < count; i++) + { + offsets[i] = offset + reader.ReadOffset32(); + } + + return new LayerList(offsets); + } +} diff --git a/src/SixLabors.Fonts/Tables/General/Colr/LayerRecord.cs b/src/SixLabors.Fonts/Tables/General/Colr/LayerRecord.cs new file mode 100644 index 000000000..a4c01ec86 --- /dev/null +++ b/src/SixLabors.Fonts/Tables/General/Colr/LayerRecord.cs @@ -0,0 +1,17 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables.General.Colr; + +internal readonly struct LayerRecord +{ + public LayerRecord(ushort glyphId, ushort paletteIndex) + { + this.GlyphId = glyphId; + this.PaletteIndex = paletteIndex; + } + + public ushort GlyphId { get; } + + public ushort PaletteIndex { get; } +} diff --git a/src/SixLabors.Fonts/Tables/General/Colr/Paint.cs b/src/SixLabors.Fonts/Tables/General/Colr/Paint.cs new file mode 100644 index 000000000..14aacdad7 --- /dev/null +++ b/src/SixLabors.Fonts/Tables/General/Colr/Paint.cs @@ -0,0 +1,299 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables.General.Colr; + +// Base node +internal abstract class Paint +{ + public byte Format { get; init; } +} + +// Format 1: PaintColrLayers +internal sealed class PaintColrLayers : Paint +{ + public byte NumLayers { get; init; } // uint8 + + public uint FirstLayerIndex { get; init; } // uint32 into LayerList +} + +// Formats 2/3: PaintSolid / PaintVarSolid +internal sealed class PaintSolid : Paint +{ + public ushort PaletteIndex { get; init; } // uint16 + + public float Alpha { get; init; } // F2DOT14 +} + +internal sealed class PaintVarSolid : Paint +{ + public ushort PaletteIndex { get; init; } // uint16 + + public float Alpha { get; init; } // F2DOT14 (var via varIndexBase + 0) + + public uint VarIndexBase { get; init; } // uint32 +} + +// Formats 4/5: PaintLinearGradient / PaintVarLinearGradient +internal sealed class PaintLinearGradient : Paint +{ + public required ColorLine ColorLine { get; init; } + + public short X0 { get; init; } // FWORD + + public short Y0 { get; init; } // FWORD + + public short X1 { get; init; } // FWORD + + public short Y1 { get; init; } // FWORD + + public short X2 { get; init; } // FWORD (rotation point) + + public short Y2 { get; init; } // FWORD +} + +internal sealed class PaintVarLinearGradient : Paint +{ + public required VarColorLine ColorLine { get; init; } + + public short X0 { get; init; } // FWORD (var +0) + + public short Y0 { get; init; } // FWORD (var +1) + + public short X1 { get; init; } // FWORD (var +2) + + public short Y1 { get; init; } // FWORD (var +3) + + public short X2 { get; init; } // FWORD (var +4) + + public short Y2 { get; init; } // FWORD (var +5) + + public uint VarIndexBase { get; init; } // uint32 +} + +// Formats 6/7: PaintRadialGradient / PaintVarRadialGradient +internal sealed class PaintRadialGradient : Paint +{ + public required ColorLine ColorLine { get; init; } + + public short X0 { get; init; } // FWORD + + public short Y0 { get; init; } // FWORD + + public ushort Radius0 { get; init; } // UFWORD + + public short X1 { get; init; } // FWORD + + public short Y1 { get; init; } // FWORD + + public ushort Radius1 { get; init; } // UFWORD +} + +internal sealed class PaintVarRadialGradient : Paint +{ + public required VarColorLine ColorLine { get; init; } + + public short X0 { get; init; } // FWORD (var +0) + + public short Y0 { get; init; } // FWORD (var +1) + + public ushort Radius0 { get; init; } // UFWORD (var +2) + + public short X1 { get; init; } // FWORD (var +3) + + public short Y1 { get; init; } // FWORD (var +4) + + public ushort Radius1 { get; init; } // UFWORD (var +5) + + public uint VarIndexBase { get; init; } // uint32 +} + +// Formats 8/9: PaintSweepGradient / PaintVarSweepGradient +internal sealed class PaintSweepGradient : Paint +{ + public required ColorLine ColorLine { get; init; } + + public short CenterX { get; init; } // FWORD + + public short CenterY { get; init; } // FWORD + + public float StartAngle { get; init; } // F2DOT14 (bias per spec) + + public float EndAngle { get; init; } // F2DOT14 (bias per spec) +} + +internal sealed class PaintVarSweepGradient : Paint +{ + public required VarColorLine ColorLine { get; init; } + + public short CenterX { get; init; } // FWORD (var +0) + + public short CenterY { get; init; } // FWORD (var +1) + + public float StartAngle { get; init; } // F2DOT14 (var +2) + + public float EndAngle { get; init; } // F2DOT14 (var +3) + + public uint VarIndexBase { get; init; } // uint32 +} + +// Format 10: PaintGlyph +internal sealed class PaintGlyph : Paint +{ + public required Paint Child { get; init; } // paintOffset -> child + + public ushort GlyphId { get; init; } // uint16 +} + +// Format 11: PaintColrGlyph +internal sealed class PaintColrGlyph : Paint +{ + public ushort GlyphId { get; init; } // uint16 (into BaseGlyphList) +} + +// Formats 12/13: PaintTransform / PaintVarTransform +internal sealed class PaintTransform : Paint +{ + public required Paint Child { get; init; } + + public Affine2x3 Transform { get; init; } +} + +internal sealed class PaintVarTransform : Paint +{ + public required Paint Child { get; init; } + + public VarAffine2x3 Transform { get; init; } +} + +// Formats 14/15: PaintTranslate / PaintVarTranslate +internal sealed class PaintTranslate : Paint +{ + public required Paint Child { get; init; } + + public short Dx { get; init; } // FWORD + + public short Dy { get; init; } // FWORD +} + +internal sealed class PaintVarTranslate : Paint +{ + public required Paint Child { get; init; } + + public short Dx { get; init; } // FWORD (var +0) + + public short Dy { get; init; } // FWORD (var +1) + + public uint VarIndexBase { get; init; } // uint32 +} + +// Formats 16/17: PaintScaleAroundCenter / PaintVarScaleAroundCenter +// Formats 18/19: PaintScale / PaintVarScale +// Formats 20/21: PaintScaleUniformAroundCenter / PaintVarScaleUniformAroundCenter +// Formats 22/23: PaintScaleUniform / PaintVarScaleUniform +internal sealed class PaintScale : Paint +{ + public required Paint Child { get; init; } + + public float ScaleX { get; init; } // F2DOT14 + + public float ScaleY { get; init; } // F2DOT14 + + public short CenterX { get; init; } // FWORD (0 if not a "around center" format) + + public short CenterY { get; init; } // FWORD (0 if not a "around center" format) + + public bool AroundCenter { get; init; } // indicates which scale format family + + public bool Uniform { get; init; } // indicates uniform vs anisotropic +} + +internal sealed class PaintVarScale : Paint +{ + public required Paint Child { get; init; } + + public float ScaleX { get; init; } // (var +0) + + public float ScaleY { get; init; } // (var +1) + + public short CenterX { get; init; } // (var +2 if around center) + + public short CenterY { get; init; } // (var +3 if around center) + + public bool AroundCenter { get; init; } + + public bool Uniform { get; init; } + + public uint VarIndexBase { get; init; } +} + +// Formats 24/25/26/27: Rotate variants +internal sealed class PaintRotate : Paint +{ + public required Paint Child { get; init; } + + public float Angle { get; init; } // F2DOT14 + + public short CenterX { get; init; } // FWORD (0 if not "around center") + + public short CenterY { get; init; } // FWORD + + public bool AroundCenter { get; init; } +} + +internal sealed class PaintVarRotate : Paint +{ + public required Paint Child { get; init; } + + public float Angle { get; init; } // (var +0) + + public short CenterX { get; init; } // (var +1 if around center) + + public short CenterY { get; init; } // (var +2 if around center) + + public bool AroundCenter { get; init; } + + public uint VarIndexBase { get; init; } +} + +// Formats 28/29/30/31: Skew variants +internal sealed class PaintSkew : Paint +{ + public required Paint Child { get; init; } + + public float XSkew { get; init; } // F2DOT14 + + public float YSkew { get; init; } // F2DOT14 + + public short CenterX { get; init; } // FWORD (0 if not "around center") + + public short CenterY { get; init; } // FWORD + + public bool AroundCenter { get; init; } +} + +internal sealed class PaintVarSkew : Paint +{ + public required Paint Child { get; init; } + + public float XSkew { get; init; } // (var +0) + + public float YSkew { get; init; } // (var +1) + + public short CenterX { get; init; } // (var +2 if around center) + + public short CenterY { get; init; } // (var +3 if around center) + + public bool AroundCenter { get; init; } + + public uint VarIndexBase { get; init; } +} + +internal sealed class PaintComposite : Paint +{ + public ColrCompositeMode CompositeMode { get; init; } // uint16 + + public required Paint Source { get; init; } // paintOffset + + public required Paint Backdrop { get; init; } // paintOffset +} diff --git a/src/SixLabors.Fonts/Tables/General/Colr/VarAffine2x3.cs b/src/SixLabors.Fonts/Tables/General/Colr/VarAffine2x3.cs new file mode 100644 index 000000000..e3302e016 --- /dev/null +++ b/src/SixLabors.Fonts/Tables/General/Colr/VarAffine2x3.cs @@ -0,0 +1,26 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables.General.Colr; + +internal readonly struct VarAffine2x3 +{ + public readonly float Xx; + public readonly float Yx; + public readonly float Xy; + public readonly float Yy; + public readonly float Dx; + public readonly float Dy; + public readonly uint VarIndexBase; + + public VarAffine2x3(float xx, float yx, float xy, float yy, float dx, float dy, uint varIndexBase) + { + this.Xx = xx; + this.Yx = yx; + this.Xy = xy; + this.Yy = yy; + this.Dx = dx; + this.Dy = dy; + this.VarIndexBase = varIndexBase; + } +} diff --git a/src/SixLabors.Fonts/Tables/General/Colr/VarColorIndex.cs b/src/SixLabors.Fonts/Tables/General/Colr/VarColorIndex.cs new file mode 100644 index 000000000..761a3deed --- /dev/null +++ b/src/SixLabors.Fonts/Tables/General/Colr/VarColorIndex.cs @@ -0,0 +1,26 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; + +namespace SixLabors.Fonts.Tables.General.Colr; + +internal readonly struct VarColorIndex +{ + public VarColorIndex(ushort paletteIndex, float alpha, uint varIndexBase) + { + this.PaletteIndex = paletteIndex; + this.Alpha = alpha; + this.VarIndexBase = varIndexBase; + } + + public ushort PaletteIndex { get; } + + public float Alpha { get; } + + public uint VarIndexBase { get; } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static VarColorIndex Load(BigEndianBinaryReader reader) + => new(reader.ReadUInt16(), reader.ReadF2Dot14(), reader.ReadUInt32()); +} diff --git a/src/SixLabors.Fonts/Tables/General/Colr/VarColorLine.cs b/src/SixLabors.Fonts/Tables/General/Colr/VarColorLine.cs new file mode 100644 index 000000000..ae821effa --- /dev/null +++ b/src/SixLabors.Fonts/Tables/General/Colr/VarColorLine.cs @@ -0,0 +1,33 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables.General.Colr; + +internal sealed class VarColorLine +{ + public VarColorLine(Extend extend, VarColorStop[] stops) + { + this.Extend = extend; + this.Stops = stops; + } + + public Extend Extend { get; } + + public VarColorStop[] Stops { get; } + + public int Count => this.Stops.Length; + + public static VarColorLine Load(BigEndianBinaryReader reader) + { + Extend extend = reader.ReadByte(); + ushort numStops = reader.ReadUInt16(); + + VarColorStop[] stops = new VarColorStop[numStops]; + for (int i = 0; i < numStops; i++) + { + stops[i] = VarColorStop.Load(reader); + } + + return new VarColorLine(extend, stops); + } +} diff --git a/src/SixLabors.Fonts/Tables/General/Colr/VarColorStop.cs b/src/SixLabors.Fonts/Tables/General/Colr/VarColorStop.cs new file mode 100644 index 000000000..c146893bb --- /dev/null +++ b/src/SixLabors.Fonts/Tables/General/Colr/VarColorStop.cs @@ -0,0 +1,29 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; + +namespace SixLabors.Fonts.Tables.General.Colr; + +internal readonly struct VarColorStop +{ + public VarColorStop(float stopOffset, ushort paletteIndex, float alpha, uint varIndexBase) + { + this.StopOffset = stopOffset; + this.PaletteIndex = paletteIndex; + this.Alpha = alpha; + this.VarIndexBase = varIndexBase; + } + + public float StopOffset { get; } + + public ushort PaletteIndex { get; } + + public float Alpha { get; } + + public uint VarIndexBase { get; } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static VarColorStop Load(BigEndianBinaryReader reader) + => new(reader.ReadF2Dot14(), reader.ReadUInt16(), reader.ReadF2Dot14(), reader.ReadUInt32()); +} diff --git a/src/SixLabors.Fonts/Tables/General/CpalTable.cs b/src/SixLabors.Fonts/Tables/General/CpalTable.cs index e1f18394b..3bd484d75 100644 --- a/src/SixLabors.Fonts/Tables/General/CpalTable.cs +++ b/src/SixLabors.Fonts/Tables/General/CpalTable.cs @@ -66,15 +66,15 @@ public static CpalTable Load(BigEndianBinaryReader reader) offsetPaletteEntryLabelArray = reader.ReadOffset32(); } - reader.Seek(offsetFirstColorRecord, System.IO.SeekOrigin.Begin); - var palettes = new GlyphColor[numColorRecords]; + reader.Seek(offsetFirstColorRecord, SeekOrigin.Begin); + GlyphColor[] palettes = new GlyphColor[numColorRecords]; for (int n = 0; n < numColorRecords; n++) { byte blue = reader.ReadByte(); byte green = reader.ReadByte(); byte red = reader.ReadByte(); byte alpha = reader.ReadByte(); - palettes[n] = new GlyphColor(blue, green, red, alpha); + palettes[n] = new GlyphColor(red, green, blue, alpha); } return new CpalTable(colorRecordIndices, palettes); diff --git a/src/SixLabors.Fonts/Tables/General/MaximumProfileTable.cs b/src/SixLabors.Fonts/Tables/General/MaximumProfileTable.cs index 4372aabbe..e1d400a88 100644 --- a/src/SixLabors.Fonts/Tables/General/MaximumProfileTable.cs +++ b/src/SixLabors.Fonts/Tables/General/MaximumProfileTable.cs @@ -95,7 +95,7 @@ public static MaximumProfileTable Load(BigEndianBinaryReader reader) // uint16 | maxInstructionDefs | Number of IDEFs. // uint16 | maxStackElements | Maximum stack depth2. // uint16 | maxSizeOfInstructions | Maximum byte count for glyph instructions. - // uint16 | maxComponentElements | Maximum number of components referenced at “top level” for any composite glyph. + // uint16 | maxComponentElements | Maximum number of components referenced at "top level" for any composite glyph. // uint16 | maxComponentDepth | Maximum levels of recursion; 1 for simple components. ushort maxPoints = reader.ReadUInt16(); ushort maxContours = reader.ReadUInt16(); diff --git a/src/SixLabors.Fonts/Tables/General/Svg/SvgDocumentIndexEntry.cs b/src/SixLabors.Fonts/Tables/General/Svg/SvgDocumentIndexEntry.cs new file mode 100644 index 000000000..0e81ebc31 --- /dev/null +++ b/src/SixLabors.Fonts/Tables/General/Svg/SvgDocumentIndexEntry.cs @@ -0,0 +1,23 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tables.General.Svg; + +internal readonly struct SvgDocumentIndexEntry +{ + public SvgDocumentIndexEntry(ushort startGlyphId, ushort endGlyphId, uint svgDocOffset, uint svgDocLength) + { + this.StartGlyphId = startGlyphId; + this.EndGlyphId = endGlyphId; + this.SvgDocOffset = svgDocOffset; + this.SvgDocLength = svgDocLength; + } + + public ushort StartGlyphId { get; } + + public ushort EndGlyphId { get; } + + public uint SvgDocOffset { get; } + + public uint SvgDocLength { get; } +} diff --git a/src/SixLabors.Fonts/Tables/General/Svg/SvgGlyphSource.cs b/src/SixLabors.Fonts/Tables/General/Svg/SvgGlyphSource.cs new file mode 100644 index 000000000..8e6c6e08b --- /dev/null +++ b/src/SixLabors.Fonts/Tables/General/Svg/SvgGlyphSource.cs @@ -0,0 +1,1574 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Xml; +using System.Xml.Linq; +using SixLabors.Fonts.Rendering; + +#pragma warning disable SA1201 // Elements should appear in the correct order +namespace SixLabors.Fonts.Tables.General.Svg; + +/// +/// Supplies painted glyphs (layers + commands + paints) and canvas metadata for OT-SVG glyphs. +/// Geometry coordinates are kept in SVG user space; all transforms are carried as matrices +/// on the canvas (root) and on each layer. No point-transforming is performed here. +/// +internal sealed class SvgGlyphSource : IPaintedGlyphSource +{ + private readonly SvgTable svgTable; + private static readonly Dictionary DocCache = []; + private static readonly ConcurrentDictionary CachedGlyphs = []; + + private sealed class ParsedDoc + { + public required XDocument Doc { get; init; } + + public required Dictionary IdMap { get; init; } + } + + /// + /// Initializes a new instance of the class. + /// + /// The SVG table. + public SvgGlyphSource(SvgTable svgTable) => this.svgTable = svgTable; + + /// + public bool TryGetPaintedGlyph(ushort glyphId, out PaintedGlyph glyph, out PaintedCanvasMetadata canvas) + { + (PaintedGlyph Glyph, PaintedCanvasMetadata Canvas) result = CachedGlyphs.GetOrAdd(glyphId, gid => + { + if (this.TryGetParsedDoc(gid, out ParsedDoc? parsed)) + { + XElement? root = parsed.Doc.Root; + if (root is not null) + { + FontRectangle viewBox = GetViewBox(root); + Matrix3x2 rootTransform = ParseTransform(root.Attribute("transform")?.Value); + + // Prefer a dedicated group with id="glyph{gid}", else fall back to the root. + string wantedId = "glyph" + gid.ToString(CultureInfo.InvariantCulture); + XElement glyphRoot = parsed.IdMap.TryGetValue(wantedId, out XElement? ge) ? ge : root; + + List layers = []; + Walk( + glyphRoot, + rootTransform, + inheritedPaint: null, + outputLayers: layers, + idMap: parsed.IdMap); + + if (layers.Count > 0) + { + PaintedGlyph glyph = new(layers); + PaintedCanvasMetadata canvas = new(viewBox, true, rootTransform); + return (glyph, canvas); + } + } + } + + return (default, default); + }); + + glyph = result.Glyph; + canvas = result.Canvas; + return result.Glyph.Layers.Count > 0; + } + + private bool TryGetParsedDoc(ushort glyphId, [NotNullWhen(true)] out ParsedDoc? parsed) + { + parsed = default; + + if (!this.svgTable.TryGetDocumentSpan(glyphId, out int _, out int _)) + { + return false; + } + + if (DocCache.TryGetValue(glyphId, out parsed)) + { + return true; + } + + if (!this.svgTable.TryOpenDecodedDocumentStream(glyphId, out Stream stream)) + { + return false; + } + + using (stream) + { + XDocument doc = LoadXml(stream); + if (doc.Root is null) + { + return false; + } + + // TODO: How large is this likely to get? If large, consider a more memory-efficient structure. + Dictionary idMap = new(1024, StringComparer.Ordinal); + + foreach (XElement e in doc.Root.DescendantsAndSelf()) + { + XAttribute? id = e.Attribute("id"); + if (id is not null) + { + idMap[id.Value] = e; // last-wins + } + } + + parsed = new ParsedDoc + { + Doc = doc, + IdMap = idMap + }; + + DocCache[glyphId] = parsed; + return true; + } + } + + private static XDocument LoadXml(Stream stream) + { + XmlReaderSettings settings = new() + { + DtdProcessing = DtdProcessing.Ignore, + XmlResolver = null, + IgnoreComments = true, + IgnoreProcessingInstructions = true, + IgnoreWhitespace = true + }; + + using XmlReader reader = XmlReader.Create(stream, settings); + return XDocument.Load(reader, LoadOptions.None); + } + + private static FontRectangle GetViewBox(XElement svg) + { + if (TryParseViewBox(svg.Attribute("viewBox")?.Value, out float x, out float y, out float w, out float h)) + { + return new FontRectangle(x, y, w, h); + } + + // No viewBox; return an empty rect. Metrics layer must decide fallback mapping. + return FontRectangle.Empty; + } + + private static void Walk( + XElement node, + Matrix3x2 parentLocalTransform, + Paint? inheritedPaint, + List outputLayers, + Dictionary idMap) + { + Matrix3x2 localTransform = parentLocalTransform * ParseTransform(node.Attribute("transform")?.Value); + + FillRule fillRule = ResolveFillRule(node, FillRule.NonZero); + Paint? paint = ResolvePaint(node, inheritedPaint, idMap, out bool fillNone, out float opacityMul); + + string name = node.Name.LocalName; + switch (name) + { + case "svg": + case "g": + { + foreach (XElement child in node.Elements()) + { + Walk(child, localTransform, fillNone ? null : paint, outputLayers, idMap); + } + + break; + } + + case "use": + { + string? href = GetHref(node); + if (href is null) + { + break; + } + + float ux = ParseFloat(node.Attribute("x")?.Value); + float uy = ParseFloat(node.Attribute("y")?.Value); + Matrix3x2 xf = localTransform * Matrix3x2.CreateTranslation(ux, uy); + + Paint? usePaint = ResolvePaint(node, paint, idMap, out bool useNone, out float _); + Paint? childInherited = useNone ? null : usePaint; + + XElement? target = LookupById(idMap, href); + if (target is not null) + { + Walk(target, xf, childInherited, outputLayers, idMap); + } + + break; + } + + case "path": + { + if (fillNone) + { + break; + } + + string? d = node.Attribute("d")?.Value; + if (string.IsNullOrWhiteSpace(d)) + { + break; + } + + List cmds = BuildCommandsFromPathData(d); + if (cmds.Count > 0) + { + Paint? layerPaint = ApplyOpacityToPaint(paint, opacityMul); + outputLayers.Add(new(layerPaint, fillRule, localTransform, null, cmds)); + } + + break; + } + + case "polygon": + case "polyline": + { + if (fillNone) + { + break; + } + + string pts = node.Attribute("points")?.Value ?? string.Empty; + float[] coords = ParseFloatList(pts); + if (coords.Length >= 4) + { + bool close = string.Equals(node.Name.LocalName, "polygon", StringComparison.Ordinal); + List cmds = BuildCommandsFromPoly(coords, close); + if (cmds.Count > 0) + { + Paint? layerPaint = ApplyOpacityToPaint(paint, opacityMul); + outputLayers.Add(new(layerPaint, fillRule, localTransform, null, cmds)); + } + } + + break; + } + + case "rect": + { + if (fillNone) + { + break; + } + + float x = ParseFloat(node.Attribute("x")?.Value); + float y = ParseFloat(node.Attribute("y")?.Value); + float w = ParseFloat(node.Attribute("width")?.Value); + float h = ParseFloat(node.Attribute("height")?.Value); + + // TODO: Rounded corners (rx/ry) not handled here (could be approximated later if needed). + if (w > 0f && h > 0f) + { + float[] coords = + [ + x, y, + x + w, y, + x + w, y + h, + x, y + h + ]; + + List cmds = BuildCommandsFromPoly(coords, close: true); + if (cmds.Count > 0) + { + Paint? layerPaint = ApplyOpacityToPaint(paint, opacityMul); + outputLayers.Add(new(layerPaint, fillRule, localTransform, null, cmds)); + } + } + + break; + } + + case "circle": + { + if (fillNone) + { + break; + } + + float cx = ParseFloat(node.Attribute("cx")?.Value); + float cy = ParseFloat(node.Attribute("cy")?.Value); + float r = ParseFloat(node.Attribute("r")?.Value); + if (r > 0f) + { + List cmds = BuildCommandsForEllipse(cx, cy, r, r); + if (cmds.Count > 0) + { + Paint? layerPaint = ApplyOpacityToPaint(paint, opacityMul); + outputLayers.Add(new(layerPaint, fillRule, localTransform, null, cmds)); + } + } + + break; + } + + case "ellipse": + { + if (fillNone) + { + break; + } + + float cx = ParseFloat(node.Attribute("cx")?.Value); + float cy = ParseFloat(node.Attribute("cy")?.Value); + float rx = ParseFloat(node.Attribute("rx")?.Value); + float ry = ParseFloat(node.Attribute("ry")?.Value); + if (rx > 0f && ry > 0f) + { + List cmds = BuildCommandsForEllipse(cx, cy, rx, ry); + if (cmds.Count > 0) + { + Paint? layerPaint = ApplyOpacityToPaint(paint, opacityMul); + outputLayers.Add(new(layerPaint, fillRule, localTransform, null, cmds)); + } + } + + break; + } + + default: + { + // Unhandled (image, text, mask, clipPath, etc.) in v1. + break; + } + } + } + + private static Paint? ApplyOpacityToPaint(Paint? basePaint, float opacityMul) + { + if (basePaint is null) + { + return null; + } + + float effective = Math.Clamp(basePaint.Opacity * opacityMul, 0f, 1f); + if (effective <= 0f) + { + return null; + } + + return basePaint switch + { + SolidPaint s => new SolidPaint { Color = s.Color, Opacity = effective }, + LinearGradientPaint lg => new LinearGradientPaint + { + Units = lg.Units, + P0 = lg.P0, + P1 = lg.P1, + Spread = lg.Spread, + Stops = lg.Stops, + Transform = lg.Transform, + Opacity = effective + }, + RadialGradientPaint rg => new RadialGradientPaint + { + Units = rg.Units, + Center0 = rg.Center0, + Radius0 = rg.Radius0, + Center1 = rg.Center1, + Radius1 = rg.Radius1, + Spread = rg.Spread, + Stops = rg.Stops, + Transform = rg.Transform, + Opacity = effective + }, + _ => null, + }; + } + + private static FillRule ResolveFillRule(XElement e, FillRule inheritedDefault) + { + string? styleRule = TryCss(e.Attribute("style")?.Value, "fill-rule"); + string? attrRule = e.Attribute("fill-rule")?.Value; + string? value = styleRule ?? attrRule; + + if (string.Equals(value, "evenodd", StringComparison.OrdinalIgnoreCase)) + { + return FillRule.EvenOdd; + } + + if (string.Equals(value, "nonzero", StringComparison.OrdinalIgnoreCase)) + { + return FillRule.NonZero; + } + + return inheritedDefault; + } + + private static Paint? ResolvePaint( + XElement e, + Paint? inherited, + Dictionary idMap, + out bool fillNone, + out float opacityMul) + { + fillNone = false; + opacityMul = 1f; + + string? style = e.Attribute("style")?.Value; + string? fillAttr = e.Attribute("fill")?.Value; + string? opacityAttr = e.Attribute("opacity")?.Value; + string? fillOpacityAttr = e.Attribute("fill-opacity")?.Value; + + string? styleFill = TryCss(style, "fill"); + string? styleOpacity = TryCss(style, "opacity"); + string? styleFillOpacity = TryCss(style, "fill-opacity"); + + string? fill = styleFill ?? fillAttr; + string? op = styleOpacity ?? opacityAttr; + string? fop = styleFillOpacity ?? fillOpacityAttr; + + if (!string.IsNullOrEmpty(op) && float.TryParse(op, NumberStyles.Float, CultureInfo.InvariantCulture, out float o)) + { + opacityMul *= Math.Clamp(o, 0f, 1f); + } + + if (!string.IsNullOrEmpty(fop) && float.TryParse(fop, NumberStyles.Float, CultureInfo.InvariantCulture, out float fo)) + { + opacityMul *= Math.Clamp(fo, 0f, 1f); + } + + if (string.IsNullOrEmpty(fill)) + { + return inherited; + } + + if (string.Equals(fill, "none", StringComparison.OrdinalIgnoreCase)) + { + fillNone = true; + return null; + } + + if (TryParseColor(fill, out GlyphColor color)) + { + return new SolidPaint { Color = color }; + } + + if (TryExtractUrlId(fill, out string? paintId) && paintId is not null) + { + return ResolvePaintServer(paintId, idMap) ?? inherited; + } + + return inherited; + } + + private static Paint? ResolvePaintServer(string id, Dictionary idMap) + { + if (!idMap.TryGetValue(id, out XElement? server)) + { + return null; + } + + string tag = server.Name.LocalName; + return tag switch + { + // SVG only has linearGradient and radialGradient. + "linearGradient" => BuildLinearGradient(server, idMap), + "radialGradient" => BuildRadialGradient(server, idMap), + _ => null + }; + } + + private static LinearGradientPaint? BuildLinearGradient(XElement grad, Dictionary idMap) + { + GradientUnits units = GradientUnits.ObjectBoundingBox; + SpreadMethod spread = SpreadMethod.Pad; + Matrix3x2 gxf = Matrix3x2.Identity; + + float? x1 = null, y1 = null, x2 = null, y2 = null; + List<(float Offset, GlyphColor Color)> stops = []; + + HashSet visited = new(StringComparer.Ordinal); + XElement? cur = grad; + + while (cur is not null) + { + string? u = cur.Attribute("gradientUnits")?.Value; + if (u is not null) + { + units = ParseGradientUnits(u); + } + + string? sm = cur.Attribute("spreadMethod")?.Value; + if (sm is not null) + { + spread = ParseSpreadMethod(sm); + } + + gxf = ParseTransform(cur.Attribute("gradientTransform")?.Value) * gxf; + + x1 ??= ParseCoordNullable(cur.Attribute("x1")?.Value); + y1 ??= ParseCoordNullable(cur.Attribute("y1")?.Value); + x2 ??= ParseCoordNullable(cur.Attribute("x2")?.Value); + y2 ??= ParseCoordNullable(cur.Attribute("y2")?.Value); + + bool hadStops = false; + foreach (XElement s in cur.Elements()) + { + if (s.Name.LocalName != "stop") + { + continue; + } + + if (TryParseStop(s, out float off, out GlyphColor c)) + { + stops.Add((off, c)); + hadStops = true; + } + } + + if (hadStops) + { + break; + } + + string? href = GetHref(cur); + if (href is null || href.Length <= 1 || href[0] != '#') + { + break; + } + + string refId = href[1..]; + if (!visited.Add(refId) || !idMap.TryGetValue(refId, out cur)) + { + break; + } + } + + if (!x1.HasValue) + { + x1 = 0f; + } + + if (!y1.HasValue) + { + y1 = 0f; + } + + if (!x2.HasValue) + { + x2 = units == GradientUnits.ObjectBoundingBox ? 1f : 0f; + } + + if (!y2.HasValue) + { + y2 = 0f; + } + + GradientStop[] gs = BuildStopsArray(stops); + + return new LinearGradientPaint + { + Units = units, + P0 = new Vector2(x1.Value, y1.Value), + P1 = new Vector2(x2.Value, y2.Value), + Spread = spread, + Stops = gs, + Transform = gxf + }; + } + + private static RadialGradientPaint? BuildRadialGradient(XElement grad, Dictionary idMap) + { + GradientUnits units = GradientUnits.ObjectBoundingBox; + SpreadMethod spread = SpreadMethod.Pad; + Matrix3x2 gxf = Matrix3x2.Identity; + + float? cx = null, cy = null, r = null, fx = null, fy = null, fr = null; + List<(float Offset, GlyphColor Color)> stops = []; + + HashSet visited = new(StringComparer.Ordinal); + XElement? cur = grad; + + while (cur is not null) + { + string? u = cur.Attribute("gradientUnits")?.Value; + if (u is not null) + { + units = ParseGradientUnits(u); + } + + string? sm = cur.Attribute("spreadMethod")?.Value; + if (sm is not null) + { + spread = ParseSpreadMethod(sm); + } + + gxf = ParseTransform(cur.Attribute("gradientTransform")?.Value) * gxf; + + cx ??= ParseCoordNullable(cur.Attribute("cx")?.Value); + cy ??= ParseCoordNullable(cur.Attribute("cy")?.Value); + r ??= ParseRadiusNullable(cur.Attribute("r")?.Value); + fx ??= ParseCoordNullable(cur.Attribute("fx")?.Value); + fy ??= ParseCoordNullable(cur.Attribute("fy")?.Value); + fr ??= ParseRadiusNullable(cur.Attribute("fr")?.Value); + + bool hadStops = false; + foreach (XElement s in cur.Elements()) + { + if (s.Name.LocalName != "stop") + { + continue; + } + + if (TryParseStop(s, out float off, out GlyphColor c)) + { + stops.Add((off, c)); + hadStops = true; + } + } + + if (hadStops) + { + break; + } + + string? href = GetHref(cur); + if (href is null || href.Length <= 1 || href[0] != '#') + { + break; + } + + string refId = href[1..]; + if (!visited.Add(refId) || !idMap.TryGetValue(refId, out cur)) + { + break; + } + } + + if (!cx.HasValue) + { + cx = units == GradientUnits.ObjectBoundingBox ? 0.5f : 0f; + } + + if (!cy.HasValue) + { + cy = units == GradientUnits.ObjectBoundingBox ? 0.5f : 0f; + } + + if (!r.HasValue) + { + r = units == GradientUnits.ObjectBoundingBox ? 0.5f : 0f; + } + + if (!fx.HasValue) + { + fx = cx.Value; + } + + if (!fy.HasValue) + { + fy = cy.Value; + } + + if (!fr.HasValue) + { + fr = 0f; + } + + GradientStop[] gs = BuildStopsArray(stops); + + // Center0=(fx,fy), Radius0=fr; Center1=(cx,cy), Radius1=r + return new RadialGradientPaint + { + Units = units, + Center0 = new Vector2(fx.Value, fy.Value), + Radius0 = fr.Value, + Center1 = new Vector2(cx.Value, cy.Value), + Radius1 = r.Value, + Spread = spread, + Stops = gs, + Transform = gxf + }; + } + + private static SpreadMethod ParseSpreadMethod(string value) + { + if (string.Equals(value, "reflect", StringComparison.OrdinalIgnoreCase)) + { + return SpreadMethod.Reflect; + } + + if (string.Equals(value, "repeat", StringComparison.OrdinalIgnoreCase)) + { + return SpreadMethod.Repeat; + } + + return SpreadMethod.Pad; + } + + private static GradientUnits ParseGradientUnits(string value) + => string.Equals(value, "userSpaceOnUse", StringComparison.OrdinalIgnoreCase) + ? GradientUnits.UserSpaceOnUse + : GradientUnits.ObjectBoundingBox; + + private static float? ParseCoordNullable(string? s) + { + if (string.IsNullOrEmpty(s)) + { + return null; + } + + if (s!.EndsWith('%')) + { + if (float.TryParse(s.AsSpan(0, s.Length - 1), NumberStyles.Float, CultureInfo.InvariantCulture, out float p)) + { + return p / 100f; + } + + return null; + } + + if (float.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out float v)) + { + return v; // In OBB this is already a fraction; in userSpace it is absolute user units. + } + + return null; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static float? ParseRadiusNullable(string? s) + => ParseCoordNullable(s); + + private static bool TryParseStop(XElement stop, out float offset, out GlyphColor color) + { + offset = 0f; + color = default; + + string? style = stop.Attribute("style")?.Value; + string? offAttr = stop.Attribute("offset")?.Value; + + string? sc = stop.Attribute("stop-color")?.Value ?? TryCss(style, "stop-color"); + string? so = stop.Attribute("stop-opacity")?.Value ?? TryCss(style, "stop-opacity"); + + if (!string.IsNullOrEmpty(offAttr)) + { + if (offAttr.EndsWith('%')) + { + if (float.TryParse(offAttr.AsSpan(0, offAttr.Length - 1), NumberStyles.Float, CultureInfo.InvariantCulture, out float p)) + { + offset = Math.Clamp(p / 100f, 0f, 1f); + } + } + else if (float.TryParse(offAttr, NumberStyles.Float, CultureInfo.InvariantCulture, out float v)) + { + offset = Math.Clamp(v, 0f, 1f); + } + } + + GlyphColor baseColor = new(0, 0, 0, 255); + if (!string.IsNullOrEmpty(sc) && TryParseColor(sc, out GlyphColor parsed)) + { + baseColor = parsed; + } + + float aMul = 1f; + if (!string.IsNullOrEmpty(so) && float.TryParse(so, NumberStyles.Float, CultureInfo.InvariantCulture, out float soVal)) + { + aMul = Math.Clamp(soVal, 0f, 1f); + } + + byte a = (byte)Math.Clamp((int)Math.Round(baseColor.A * aMul), 0, 255); + color = new GlyphColor(baseColor.R, baseColor.G, baseColor.B, a); + return true; + } + + private static GradientStop[] BuildStopsArray(List<(float Offset, GlyphColor Color)> list) + { + if (list.Count == 0) + { + return + [ + new GradientStop(0f, new GlyphColor(0, 0, 0, 255)), + new GradientStop(1f, new GlyphColor(0, 0, 0, 255)) + ]; + } + + list.Sort((a, b) => a.Offset.CompareTo(b.Offset)); + GradientStop[] stops = new GradientStop[list.Count]; + for (int i = 0; i < list.Count; i++) + { + (float o, GlyphColor c) = list[i]; + stops[i] = new GradientStop(Math.Clamp(o, 0f, 1f), c); + } + + return stops; + } + + private static string? TryCss(string? style, string prop) + { + if (string.IsNullOrEmpty(style)) + { + return null; + } + + // TODO: Rewrite this using Span.Split to avoid allocations. + string[] parts = style.Split(';', StringSplitOptions.RemoveEmptyEntries); + for (int i = 0; i < parts.Length; i++) + { + string part = parts[i]; + int c = part.IndexOf(':'); + if (c <= 0) + { + continue; + } + + string name = part.AsSpan(0, c).Trim().ToString(); + if (name.Equals(prop, StringComparison.OrdinalIgnoreCase)) + { + return part.AsSpan(c + 1).Trim().ToString(); + } + } + + return null; + } + + private static bool TryParseColor(string s, out GlyphColor color) + { + if (GlyphColor.TryParseNamed(s, out color)) + { + return true; + } + + if (GlyphColor.TryParseHex(s, out GlyphColor hex)) + { + color = hex; + return true; + } + + if (s.StartsWith("rgb", StringComparison.OrdinalIgnoreCase)) + { + int l = s.IndexOf('('); + int r = s.IndexOf(')'); + if (l >= 0 && r > l) + { + // TODO: Rewrite this using Span.Split to avoid allocations. + string[] comps = s.Substring(l + 1, r - l - 1).Split(','); + if (comps.Length >= 3) + { + byte rr = ParseByte(comps[0]); + byte gg = ParseByte(comps[1]); + byte bb = ParseByte(comps[2]); + byte aa = 255; + if (comps.Length >= 4 && float.TryParse(comps[3], NumberStyles.Float, CultureInfo.InvariantCulture, out float af)) + { + aa = (byte)Math.Clamp((int)Math.Round(255f * af), 0, 255); + } + + color = new GlyphColor(rr, gg, bb, aa); + return true; + } + } + } + + return false; + + static byte ParseByte(ReadOnlySpan x) + { + if (x.IsEmpty) + { + return 0; + } + + ReadOnlySpan t = x.Trim(); + if (t[^1] == '%') + { + if (float.TryParse(t[..^1], NumberStyles.Float, CultureInfo.InvariantCulture, out float p)) + { + return (byte)Math.Clamp((int)Math.Round(255f * (p / 100f)), 0, 255); + } + + return 0; + } + + if (int.TryParse(t, NumberStyles.Integer, CultureInfo.InvariantCulture, out int v)) + { + return (byte)Math.Clamp(v, 0, 255); + } + + return 0; + } + } + + private static bool TryExtractUrlId(string s, [NotNullWhen(true)] out string? id) + { + id = null; + + int lp = s.IndexOf("url(", StringComparison.OrdinalIgnoreCase); + if (lp < 0) + { + return false; + } + + int rp = s.IndexOf(')', lp + 4); + if (rp < 0) + { + return false; + } + + string inner = s[(lp + 4)..rp].Trim(); + if (inner.Length > 1 && inner[0] == '#') + { + id = inner[1..]; + return true; + } + + return false; + } + + private static XElement? LookupById(Dictionary idMap, string href) + { + if (string.IsNullOrEmpty(href) || href[0] != '#') + { + return null; + } + + return idMap.TryGetValue(href.AsSpan(1).ToString(), out XElement? e) ? e : null; + } + + private static string? GetHref(XElement e) + { + XNamespace xlink = "http://www.w3.org/1999/xlink"; + return e.Attribute(xlink + "href")?.Value ?? e.Attribute("href")?.Value; + } + + private static List BuildCommandsFromPoly(float[] coords, bool close) + { + List cmds = []; + + Vector2 start = new(coords[0], coords[1]); + cmds.Add(PathCommand.MoveTo(start)); + + Vector2 prev = start; + for (int i = 2; i + 1 < coords.Length; i += 2) + { + Vector2 p = new(coords[i], coords[i + 1]); + if (!NearlyEqual(prev, p)) + { + cmds.Add(PathCommand.LineTo(p)); + prev = p; + } + } + + if (close && !NearlyEqual(prev, start)) + { + cmds.Add(PathCommand.LineTo(start)); + cmds.Add(PathCommand.Close()); + } + + return cmds; + } + + private static List BuildCommandsForEllipse(float cx, float cy, float rx, float ry) + { + List cmds = []; + + // Start at (cx + rx, cy) + Vector2 s = new(cx + rx, cy); + cmds.Add(PathCommand.MoveTo(s)); + + // First half to (cx - rx, cy) + Vector2 p1 = new(cx - rx, cy); + cmds.Add(PathCommand.ArcTo(rx, ry, 0f, true, true, p1)); + + // Second half back to start + Vector2 p2 = new(cx + rx, cy); + cmds.Add(PathCommand.ArcTo(rx, ry, 0f, true, true, p2)); + + cmds.Add(PathCommand.Close()); + return cmds; + } + + private static List BuildCommandsFromPathData(string d) + { + List cmds = []; + + ReadOnlySpan s = d.AsSpan(); + + Vector2 first = default; + Vector2 curr = default; + Vector2 lastc = default; + + Vector2 p1, p2, p3; + + char op = '\0'; + char prevOp = '\0'; + bool rel = false; + bool figureOpen = false; + + while (true) + { + s = s.TrimStart(); + if (s.Length == 0) + { + break; + } + + char ch = s[0]; + if (char.IsDigit(ch) || ch == '-' || ch == '+' || ch == '.') + { + if (s.Length == 0 || op == 'Z') + { + return []; + } + } + else if (IsSeparator(ch)) + { + s = TrimSeparator(s); + } + else + { + op = ch; + rel = false; + if (char.IsLower(op)) + { + op = char.ToUpper(op, CultureInfo.InvariantCulture); + rel = true; + } + + s = TrimSeparator(s[1..]); + } + + switch (op) + { + case 'M': + { + s = FindPoint(s, rel, curr, out p1); + + if (figureOpen) + { + cmds.Add(PathCommand.Close()); + } + + cmds.Add(PathCommand.MoveTo(p1)); + first = curr = p1; + prevOp = '\0'; + op = 'L'; + figureOpen = true; + break; + } + + case 'L': + { + s = FindPoint(s, rel, curr, out p1); + if (!NearlyEqual(p1, curr)) + { + cmds.Add(PathCommand.LineTo(p1)); + } + + curr = p1; + break; + } + + case 'H': + { + s = FindScaler(s, out float x); + if (rel) + { + x += curr.X; + } + + p1 = new Vector2(x, curr.Y); + if (!NearlyEqual(p1, curr)) + { + cmds.Add(PathCommand.LineTo(p1)); + } + + curr = p1; + break; + } + + case 'V': + { + s = FindScaler(s, out float y); + if (rel) + { + y += curr.Y; + } + + p1 = new Vector2(curr.X, y); + if (!NearlyEqual(p1, curr)) + { + cmds.Add(PathCommand.LineTo(p1)); + } + + curr = p1; + break; + } + + case 'C': + { + s = FindPoint(s, rel, curr, out p1); + s = FindPoint(s, rel, curr, out p2); + s = FindPoint(s, rel, curr, out p3); + + cmds.Add(PathCommand.CubicTo(p1, p2, p3)); + + lastc = p2; + curr = p3; + break; + } + + case 'S': + { + s = FindPoint(s, rel, curr, out p2); + s = FindPoint(s, rel, curr, out p3); + + p1 = curr; + if (prevOp is 'C' or 'S') + { + p1.X -= lastc.X - curr.X; + p1.Y -= lastc.Y - curr.Y; + } + + cmds.Add(PathCommand.CubicTo(p1, p2, p3)); + + lastc = p2; + curr = p3; + break; + } + + case 'Q': + { + s = FindPoint(s, rel, curr, out p1); + s = FindPoint(s, rel, curr, out p2); + + cmds.Add(PathCommand.QuadraticTo(p1, p2)); + + lastc = p1; + curr = p2; + break; + } + + case 'T': + { + s = FindPoint(s, rel, curr, out p2); + + p1 = curr; + if (prevOp is 'Q' or 'T') + { + p1.X -= lastc.X - curr.X; + p1.Y -= lastc.Y - curr.Y; + } + + cmds.Add(PathCommand.QuadraticTo(p1, p2)); + + lastc = p1; + curr = p2; + break; + } + + case 'A': + { + if (TryFindScaler(ref s, out float rx) + && TryTrimSeparator(ref s) + && TryFindScaler(ref s, out float ry) + && TryTrimSeparator(ref s) + && TryFindScaler(ref s, out float angle) + && TryTrimSeparator(ref s) + && TryFindScaler(ref s, out float largeArc) + && TryTrimSeparator(ref s) + && TryFindScaler(ref s, out float sweep) + && TryFindPoint(ref s, rel, curr, out p1)) + { + cmds.Add(PathCommand.ArcTo(rx, ry, angle, largeArc == 1, sweep == 1, p1)); + curr = p1; + } + + break; + } + + case 'Z': + { + if (figureOpen) + { + if (!NearlyEqual(curr, first)) + { + cmds.Add(PathCommand.LineTo(first)); + } + + cmds.Add(PathCommand.Close()); + curr = first; + figureOpen = false; + } + + break; + } + + default: + { + return []; + } + } + + if (prevOp == 0) + { + first = curr; + } + + prevOp = op; + if (op == 'M') + { + figureOpen = true; + } + } + + return cmds; + } + + private static bool TryParseViewBox(string? s, out float x, out float y, out float w, out float h) + { + x = 0f; + y = 0f; + w = 0f; + h = 0f; + + if (string.IsNullOrEmpty(s)) + { + return false; + } + + float[] v = ParseFloatList(s); + if (v.Length == 4) + { + x = v[0]; + y = v[1]; + w = v[2]; + h = v[3]; + return true; + } + + return false; + } + + private static Matrix3x2 ParseTransform(string? s) + { + if (string.IsNullOrEmpty(s)) + { + return Matrix3x2.Identity; + } + + Matrix3x2 m = Matrix3x2.Identity; + int i = 0; + int n = s.Length; + + while (i < n) + { + SkipSep(s, ref i); + if (i >= n) + { + break; + } + + int start = i; + while (i < n && char.IsLetter(s[i])) + { + i++; + } + + string op = s[start..i]; + + SkipSep(s, ref i); + if (i >= n || s[i] != '(') + { + break; + } + + i++; // '(' + + int argsStart = i; + int depth = 1; + while (i < n && depth > 0) + { + if (s[i] == '(') + { + depth++; + } + else if (s[i] == ')') + { + depth--; + } + + i++; + } + + string args = s.Substring(argsStart, (i - argsStart) - 1); + float[] a = ParseFloatList(args); + + Matrix3x2 t = Matrix3x2.Identity; + switch (op) + { + case "matrix": + { + if (a.Length >= 6) + { + t = new Matrix3x2(a[0], a[1], a[2], a[3], a[4], a[5]); + } + + break; + } + + case "translate": + { + if (a.Length == 1) + { + t = Matrix3x2.CreateTranslation(a[0], 0f); + } + else if (a.Length >= 2) + { + t = Matrix3x2.CreateTranslation(a[0], a[1]); + } + + break; + } + + case "scale": + { + if (a.Length == 1) + { + t = Matrix3x2.CreateScale(a[0], a[0]); + } + else if (a.Length >= 2) + { + t = Matrix3x2.CreateScale(a[0], a[1]); + } + + break; + } + + case "rotate": + { + if (a.Length >= 1) + { + t = Matrix3x2.CreateRotation(a[0] * (float)(Math.PI / 180.0)); + } + + break; + } + + case "skewX": + { + if (a.Length >= 1) + { + t = new Matrix3x2(1f, 0f, MathF.Tan(a[0] * (float)(Math.PI / 180.0)), 1f, 0f, 0f); + } + + break; + } + + case "skewY": + { + if (a.Length >= 1) + { + t = new Matrix3x2(1f, MathF.Tan(a[0] * (float)(Math.PI / 180.0)), 0f, 1f, 0f, 0f); + } + + break; + } + } + + m *= t; + SkipSep(s, ref i); + } + + return m; + + static void SkipSep(string s, ref int i) + { + int n = s.Length; + while (i < n) + { + char c = s[i]; + if (char.IsWhiteSpace(c) || c == ',') + { + i++; + } + else + { + break; + } + } + } + } + + private static ReadOnlySpan FindPoint(ReadOnlySpan str, bool rel, Vector2 current, out Vector2 value) + { + str = FindScaler(str, out float x); + str = FindScaler(str, out float y); + + if (rel) + { + x += current.X; + y += current.Y; + } + + value = new Vector2(x, y); + return str; + } + + private static ReadOnlySpan FindScaler(ReadOnlySpan str, out float scaler) + { + str = TrimSeparator(str); + scaler = 0f; + + for (int i = 0; i < str.Length; i++) + { + if (IsSeparator(str[i])) + { + scaler = ParseFloat(str[..i]); + return str[i..]; + } + } + + if (str.Length > 0) + { + scaler = ParseFloat(str); + } + + return []; + } + + private static bool TryTrimSeparator(ref ReadOnlySpan str) + { + ReadOnlySpan result = TrimSeparator(str); + if (str[^result.Length..].StartsWith(result)) + { + str = result; + return true; + } + + return false; + } + + private static bool TryFindScaler(ref ReadOnlySpan str, out float value) + { + ReadOnlySpan result = FindScaler(str, out float v); + if (str[^result.Length..].StartsWith(result)) + { + value = v; + str = result; + return true; + } + + value = default; + return false; + } + + private static bool TryFindPoint(ref ReadOnlySpan str, bool relative, Vector2 current, out Vector2 value) + { + ReadOnlySpan result = FindPoint(str, relative, current, out Vector2 v); + if (str[^result.Length..].StartsWith(result)) + { + value = v; + str = result; + return true; + } + + value = default; + return false; + } + + private static bool IsSeparator(char ch) + => char.IsWhiteSpace(ch) || ch == ','; + + private static ReadOnlySpan TrimSeparator(ReadOnlySpan s) + { + int idx = 0; + for (; idx < s.Length; idx++) + { + if (!IsSeparator(s[idx])) + { + break; + } + } + + return s[idx..]; + } + + private static float ParseFloat(ReadOnlySpan str) + => str.IsEmpty ? 0 : float.Parse(str, CultureInfo.InvariantCulture); + + private static float[] ParseFloatList(string s) + { + if (string.IsNullOrEmpty(s)) + { + return []; + } + + List vals = []; + int i = 0; + int n = s.Length; + + while (i < n) + { + while (i < n && (char.IsWhiteSpace(s[i]) || s[i] == ',')) + { + i++; + } + + if (i >= n) + { + break; + } + + int start = i; + + if (s[i] is '+' or '-') + { + i++; + } + + bool dot = false; + while (i < n) + { + char c = s[i]; + if (char.IsDigit(c)) + { + i++; + continue; + } + + if (c == '.' && !dot) + { + dot = true; + i++; + continue; + } + + break; + } + + if (i < n && (s[i] == 'e' || s[i] == 'E')) + { + i++; + if (i < n && (s[i] == '+' || s[i] == '-')) + { + i++; + } + + while (i < n && char.IsDigit(s[i])) + { + i++; + } + } + + if (float.TryParse(s.AsSpan(start, i - start), NumberStyles.Float, CultureInfo.InvariantCulture, out float v)) + { + vals.Add(v); + } + } + + return [.. vals]; + } + + private static bool NearlyEqual(in Vector2 a, in Vector2 b, float eps = 1e-3f) + => MathF.Abs(a.X - b.X) <= eps && MathF.Abs(a.Y - b.Y) <= eps; +} diff --git a/src/SixLabors.Fonts/Tables/General/Svg/SvgTable.cs b/src/SixLabors.Fonts/Tables/General/Svg/SvgTable.cs new file mode 100644 index 000000000..d853b5ee6 --- /dev/null +++ b/src/SixLabors.Fonts/Tables/General/Svg/SvgTable.cs @@ -0,0 +1,215 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.IO.Compression; +using System.Runtime.CompilerServices; + +namespace SixLabors.Fonts.Tables.General.Svg; + +internal class SvgTable : Table +{ + internal const string TableName = "SVG "; + private readonly byte[] tableData; + private readonly uint svgDocIndexOffset; + private readonly uint tableBaseOffset; + private readonly SvgDocumentIndexEntry[] entries; + + private SvgTable(byte[] tableData, uint svgDocIndexOffset, uint tableBaseOffset, SvgDocumentIndexEntry[] entries) + { + this.tableData = tableData; + this.svgDocIndexOffset = svgDocIndexOffset; + this.tableBaseOffset = tableBaseOffset; + this.entries = entries; + } + + public static SvgTable? Load(FontReader fontReader) + { + if (!fontReader.TryGetReaderAtTablePosition(TableName, out BigEndianBinaryReader? binaryReader)) + { + return null; + } + + using (binaryReader) + { + return Load(binaryReader); + } + } + + public static SvgTable Load(BigEndianBinaryReader reader) + { + // HEADER + // | Type | Name | Description | + // | ---------| ------------------| ----------------------------------------------------------| + // | uint16 | version | Table version number(starts at 0). | + // | Offset32 | svgDocIndexOffset | Offset(from beginning of SVG table) to SVG Document Index.| + // | uint32 | reserved | Reserved; set to 0 + ushort version = reader.ReadUInt16(); + if (version != 0) + { + throw new NotSupportedException($"Only SVG table version 0 is supported. Found version {version}."); + } + + uint svgDocIndexOffset = reader.ReadUInt32(); + _ = reader.ReadUInt32(); // reserved + + // SVG Document Index + // | Type | Name | Description | + // | ------------------| -----------| ------------------------------------------------------------| + // | uint16 | numEntries | Number of entries in the SVG Document Index. | + // | Entry[numEntries] | entries | Array of SVG Document Index Entries(sorted by startGlyphID).| + reader.Seek(svgDocIndexOffset, SeekOrigin.Begin); + ushort numEntries = reader.ReadUInt16(); + SvgDocumentIndexEntry[] entries = new SvgDocumentIndexEntry[numEntries]; + + // SVG Document Index Entry + // | Type | Name | Description | + // | ---------| -------------| -----------------------------------------------------------------------------------------| + // | uint16 | startGlyphID | First glyph ID in this range(inclusive). | + // | uint16 | endGlyphID | Last glyph ID in this range(inclusive). | + // | Offset32 | svgDocOffset | Offset from the beginning of the SVG Document Index to an SVG document. Must be non-zero.| + + // Track min relative offset from the Document Index and absolute max end. + uint minRelOffset = uint.MaxValue; + uint maxEnd = 0; + for (int i = 0; i < numEntries; i++) + { + ushort startGlyphId = reader.ReadUInt16(); + ushort endGlyphId = reader.ReadUInt16(); + uint svgDocOffset = reader.ReadUInt32(); + uint svgDocLength = reader.ReadUInt32(); + + if (svgDocOffset == 0 || svgDocLength == 0) + { + throw new InvalidFontFileException("SVG table contains an entry with zero offset or length."); + } + + if (svgDocOffset < minRelOffset) + { + minRelOffset = svgDocOffset; + } + + // Track the farthest byte we need to cover in the table buffer. + uint absEnd = svgDocIndexOffset + svgDocOffset + svgDocLength; + if (absEnd > maxEnd) + { + maxEnd = absEnd; + } + + entries[i] = new SvgDocumentIndexEntry(startGlyphId, endGlyphId, svgDocOffset, svgDocLength); + } + + // Read exactly the covered range. + uint tableStart = svgDocIndexOffset + minRelOffset; + int byteCount = (int)(maxEnd - tableStart); + + reader.Seek(tableStart, SeekOrigin.Begin); + byte[] tableData = reader.ReadBytes(byteCount); + + return new SvgTable(tableData, svgDocIndexOffset, tableStart, entries); + } + + /// + /// Returns true if the SVG Document Index contains a document for . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool ContainsGlyph(ushort glyphId) + => this.TryFindEntry(glyphId, out _); + + /// + /// Get the encoded document slice for a glyph without opening a stream. + /// + public bool TryGetDocumentSpan(ushort glyphId, out int start, out int length) + { + if (this.TryFindEntry(glyphId, out SvgDocumentIndexEntry e)) + { + start = (int)((this.svgDocIndexOffset + e.SvgDocOffset) - this.tableBaseOffset); + length = (int)e.SvgDocLength; + return true; + } + + start = 0; + length = 0; + return false; + } + + /// + /// Open a decoding stream. If the payload is gzip (RFC1952), wraps a GZipStream, + /// otherwise returns the raw memory stream. Caller owns the returned stream. + /// + public bool TryOpenDecodedDocumentStream(ushort glyphId, out Stream stream) + { + if (!this.TryOpenEncodedDocumentStream(glyphId, out Stream encoded)) + { + stream = Stream.Null; + return false; + } + + if (encoded is MemoryStream ms && ms.Length >= 2) + { + long pos = ms.Position; + int b0 = ms.ReadByte(); + int b1 = ms.ReadByte(); + ms.Position = pos; + + // Start of GZIP (RFC1952) + if (b0 == 0x1F && b1 == 0x8B) + { + stream = new GZipStream(ms, CompressionMode.Decompress, leaveOpen: false); + return true; + } + } + + stream = encoded; // plain UTF-8 XML + return true; + } + + private bool TryOpenEncodedDocumentStream(ushort glyphId, out Stream stream) + { + if (this.TryFindEntry(glyphId, out SvgDocumentIndexEntry e)) + { + int start = (int)((this.svgDocIndexOffset + e.SvgDocOffset) - this.tableBaseOffset); + int length = (int)e.SvgDocLength; + stream = new MemoryStream(this.tableData, start, length, writable: false); + return true; + } + + stream = Stream.Null; + return false; + } + + private bool TryFindEntry(ushort glyphId, out SvgDocumentIndexEntry entry) + { + int lo = 0; + int hi = this.entries.Length - 1; + int candidate = -1; + + while (lo <= hi) + { + int mid = (int)((uint)(lo + hi) >> 1); + ushort start = this.entries[mid].StartGlyphId; + + if (start <= glyphId) + { + candidate = mid; + lo = mid + 1; + } + else + { + hi = mid - 1; + } + } + + if (candidate >= 0) + { + SvgDocumentIndexEntry e = this.entries[candidate]; + if (glyphId <= e.EndGlyphId) + { + entry = e; + return true; + } + } + + entry = default; + return false; + } +} diff --git a/src/SixLabors.Fonts/Tables/TableLoader.cs b/src/SixLabors.Fonts/Tables/TableLoader.cs index fda699070..ac36429b7 100644 --- a/src/SixLabors.Fonts/Tables/TableLoader.cs +++ b/src/SixLabors.Fonts/Tables/TableLoader.cs @@ -8,6 +8,7 @@ using SixLabors.Fonts.Tables.General.Kern; using SixLabors.Fonts.Tables.General.Name; using SixLabors.Fonts.Tables.General.Post; +using SixLabors.Fonts.Tables.General.Svg; using SixLabors.Fonts.Tables.TrueType; using SixLabors.Fonts.Tables.TrueType.Glyphs; using SixLabors.Fonts.Tables.TrueType.Hinting; @@ -46,6 +47,7 @@ public TableLoader() this.Register(PostTable.TableName, PostTable.Load); this.Register(Cff1Table.TableName, Cff1Table.Load); this.Register(Cff2Table.TableName, Cff2Table.Load); + this.Register(SvgTable.TableName, SvgTable.Load); } public static TableLoader Default { get; } = new(); diff --git a/src/SixLabors.Fonts/Tables/TripleEncodingTable.cs b/src/SixLabors.Fonts/Tables/TripleEncodingTable.cs index 90a039fae..985a90a10 100644 --- a/src/SixLabors.Fonts/Tables/TripleEncodingTable.cs +++ b/src/SixLabors.Fonts/Tables/TripleEncodingTable.cs @@ -26,7 +26,7 @@ private void BuildTable() // The sign of X coordinate value(X sign). // The sign of Y coordinate value(Y sign). - // Please note that “Byte Count” field reflects total size of the triplet(flag, xCoordinate, yCoordinate), + // Please note that "Byte Count" field reflects total size of the triplet(flag, xCoordinate, yCoordinate), // including ‘flag’ value that is encoded in a separate stream. // Triplet Encoding diff --git a/src/SixLabors.Fonts/Tables/TrueType/Glyphs/CompositeGlyphLoader.cs b/src/SixLabors.Fonts/Tables/TrueType/Glyphs/CompositeGlyphLoader.cs index 4e4497a23..1c414c258 100644 --- a/src/SixLabors.Fonts/Tables/TrueType/Glyphs/CompositeGlyphLoader.cs +++ b/src/SixLabors.Fonts/Tables/TrueType/Glyphs/CompositeGlyphLoader.cs @@ -55,21 +55,21 @@ public static CompositeGlyphLoader LoadCompositeGlyph(BigEndianBinaryReader read if ((flags & CompositeGlyphFlags.WeHaveAScale) != 0) { - float scale = reader.ReadF2dot14(); // Format 2.14 + float scale = reader.ReadF2Dot14(); // Format 2.14 transform.M11 = scale; transform.M22 = scale; } else if ((flags & CompositeGlyphFlags.WeHaveXAndYScale) != 0) { - transform.M11 = reader.ReadF2dot14(); - transform.M22 = reader.ReadF2dot14(); + transform.M11 = reader.ReadF2Dot14(); + transform.M22 = reader.ReadF2Dot14(); } else if ((flags & CompositeGlyphFlags.WeHaveATwoByTwo) != 0) { - transform.M11 = reader.ReadF2dot14(); - transform.M12 = reader.ReadF2dot14(); - transform.M21 = reader.ReadF2dot14(); - transform.M22 = reader.ReadF2dot14(); + transform.M11 = reader.ReadF2Dot14(); + transform.M12 = reader.ReadF2Dot14(); + transform.M21 = reader.ReadF2Dot14(); + transform.M22 = reader.ReadF2Dot14(); } composites.Add(new Composite(glyphIndex, flags, transform)); diff --git a/src/SixLabors.Fonts/Tables/TrueType/Glyphs/GlyphTable.cs b/src/SixLabors.Fonts/Tables/TrueType/Glyphs/GlyphTable.cs index 72a0d1d1b..ea3b1a81c 100644 --- a/src/SixLabors.Fonts/Tables/TrueType/Glyphs/GlyphTable.cs +++ b/src/SixLabors.Fonts/Tables/TrueType/Glyphs/GlyphTable.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Collections.Concurrent; using SixLabors.Fonts.Tables.Woff; namespace SixLabors.Fonts.Tables.TrueType.Glyphs; @@ -9,15 +10,26 @@ internal class GlyphTable : Table { internal const string TableName = "glyf"; private readonly GlyphLoader[] loaders; + private readonly ConcurrentDictionary glyphCache; public GlyphTable(GlyphLoader[] glyphLoaders) - => this.loaders = glyphLoaders; + { + this.loaders = glyphLoaders; + this.glyphCache = new(Environment.ProcessorCount, glyphLoaders.Length); + } public int GlyphCount => this.loaders.Length; // TODO: Make this non-virtual internal virtual GlyphVector GetGlyph(int index) - => this.loaders[index].CreateGlyph(this); + { + if (index < 0 || index >= this.loaders.Length) + { + return default; + } + + return this.glyphCache.GetOrAdd(index, i => this.loaders[i].CreateGlyph(this)); + } public static GlyphTable Load(FontReader reader) { @@ -36,7 +48,7 @@ public static GlyphTable Load(BigEndianBinaryReader reader, TableFormat format, EmptyGlyphLoader empty = new(fallbackEmptyBounds); int entryCount = locations.Length; int glyphCount = entryCount - 1; // last entry is a placeholder to the end of the table - var glyphs = new GlyphLoader[glyphCount]; + GlyphLoader[] glyphs = new GlyphLoader[glyphCount]; // Special case for WOFF2 format where all glyphs need to be read in one go. if (format is TableFormat.Woff2) diff --git a/src/SixLabors.Fonts/Tables/TrueType/TrueTypeFontTables.cs b/src/SixLabors.Fonts/Tables/TrueType/TrueTypeFontTables.cs index be0c2b174..ef458b816 100644 --- a/src/SixLabors.Fonts/Tables/TrueType/TrueTypeFontTables.cs +++ b/src/SixLabors.Fonts/Tables/TrueType/TrueTypeFontTables.cs @@ -7,6 +7,7 @@ using SixLabors.Fonts.Tables.General.Kern; using SixLabors.Fonts.Tables.General.Name; using SixLabors.Fonts.Tables.General.Post; +using SixLabors.Fonts.Tables.General.Svg; using SixLabors.Fonts.Tables.TrueType.Glyphs; using SixLabors.Fonts.Tables.TrueType.Hinting; @@ -70,6 +71,8 @@ public TrueTypeFontTables( public VerticalMetricsTable? Vmtx { get; set; } + public SvgTable? Svg { get; set; } + // Tables Related to TrueType Outlines // +------+-----------------------------------------------+ // | Tag | Name | diff --git a/src/SixLabors.Fonts/Tables/TrueType/TrueTypeGlyphMetrics.cs b/src/SixLabors.Fonts/Tables/TrueType/TrueTypeGlyphMetrics.cs index f4c9658d7..315649e7c 100644 --- a/src/SixLabors.Fonts/Tables/TrueType/TrueTypeGlyphMetrics.cs +++ b/src/SixLabors.Fonts/Tables/TrueType/TrueTypeGlyphMetrics.cs @@ -3,6 +3,7 @@ using System.Collections.Concurrent; using System.Numerics; +using SixLabors.Fonts.Rendering; using SixLabors.Fonts.Tables.TrueType.Glyphs; using SixLabors.Fonts.Unicode; @@ -29,8 +30,7 @@ internal TrueTypeGlyphMetrics( ushort unitsPerEM, TextAttributes textAttributes, TextDecorations textDecorations, - GlyphType glyphType = GlyphType.Standard, - GlyphColor? glyphColor = null) + GlyphType glyphType) : base( font, glyphId, @@ -43,8 +43,7 @@ internal TrueTypeGlyphMetrics( unitsPerEM, textAttributes, textDecorations, - glyphType, - glyphColor) + glyphType) => this.vector = vector; internal TrueTypeGlyphMetrics( @@ -60,8 +59,7 @@ internal TrueTypeGlyphMetrics( Vector2 offset, Vector2 scaleFactor, TextRun textRun, - GlyphType glyphType = GlyphType.Standard, - GlyphColor? glyphColor = null) + GlyphType glyphType) : base( font, glyphId, @@ -75,8 +73,7 @@ internal TrueTypeGlyphMetrics( offset, scaleFactor, textRun, - glyphType, - glyphColor) + glyphType) => this.vector = vector; /// @@ -94,8 +91,7 @@ internal override GlyphMetrics CloneForRendering(TextRun textRun) this.Offset, this.ScaleFactor, textRun, - this.GlyphType, - this.GlyphColor); + this.GlyphType); /// /// Gets the outline for the current glyph. @@ -104,7 +100,13 @@ internal override GlyphMetrics CloneForRendering(TextRun textRun) internal GlyphVector GetOutline() => this.vector; /// - internal override void RenderTo(IGlyphRenderer renderer, Vector2 location, Vector2 offset, GlyphLayoutMode mode, TextOptions options) + internal override void RenderTo( + IGlyphRenderer renderer, + int graphemeIndex, + Vector2 location, + Vector2 offset, + GlyphLayoutMode mode, + TextOptions options) { // https://www.unicode.org/faq/unsup_char.html if (ShouldSkipGlyphRendering(this.CodePoint)) @@ -125,25 +127,20 @@ internal override void RenderTo(IGlyphRenderer renderer, Vector2 location, Vecto Matrix3x2 rotation = GetRotationMatrix(mode); FontRectangle box = this.GetBoundingBox(mode, renderLocation, scaledPPEM); - GlyphRendererParameters parameters = new(this, this.TextRun, pointSize, dpi, mode); + GlyphRendererParameters parameters = new(this, this.TextRun, pointSize, dpi, mode, graphemeIndex); if (renderer.BeginGlyph(in box, in parameters)) { if (!UnicodeUtility.ShouldRenderWhiteSpaceOnly(this.CodePoint)) { - if (this.GlyphColor.HasValue && renderer is IColorGlyphRenderer colorSurface) - { - colorSurface.SetColor(this.GlyphColor.Value); - } - GlyphVector scaledVector = this.scaledVectorCache.GetOrAdd(scaledPPEM, _ => { // Create a scaled deep copy of the vector so that we do not alter // the globally cached instance. - var clone = GlyphVector.DeepClone(this.vector); + GlyphVector clone = GlyphVector.DeepClone(this.vector); Vector2 scale = new Vector2(scaledPPEM) / this.ScaleFactor; - var matrix = Matrix3x2.CreateScale(scale); + Matrix3x2 matrix = Matrix3x2.CreateScale(scale); matrix.Translation = this.Offset * scale; GlyphVector.TransformInPlace(ref clone, matrix); @@ -227,9 +224,8 @@ internal override void RenderTo(IGlyphRenderer renderer, Vector2 location, Vecto } } - this.RenderDecorationsTo(renderer, location, mode, rotation, scaledPPEM); + renderer.EndGlyph(); + this.RenderDecorationsTo(renderer, location, mode, rotation, scaledPPEM, options); } - - renderer.EndGlyph(); } } diff --git a/src/SixLabors.Fonts/Tables/Woff/Woff2Utils.cs b/src/SixLabors.Fonts/Tables/Woff/Woff2Utils.cs index 05e76d744..c742acc8f 100644 --- a/src/SixLabors.Fonts/Tables/Woff/Woff2Utils.cs +++ b/src/SixLabors.Fonts/Tables/Woff/Woff2Utils.cs @@ -442,21 +442,21 @@ private static GlyphVector ReadCompositeGlyphData(GlyphVector[] createdGlyphs, B if ((flags & CompositeGlyphFlags.WeHaveAScale) != 0) { - float scale = reader.ReadF2dot14(); + float scale = reader.ReadF2Dot14(); transform.M11 = scale; transform.M22 = scale; } else if ((flags & CompositeGlyphFlags.WeHaveXAndYScale) != 0) { - transform.M11 = reader.ReadF2dot14(); - transform.M22 = reader.ReadF2dot14(); + transform.M11 = reader.ReadF2Dot14(); + transform.M22 = reader.ReadF2Dot14(); } else if ((flags & CompositeGlyphFlags.WeHaveATwoByTwo) != 0) { - transform.M11 = reader.ReadF2dot14(); - transform.M12 = reader.ReadF2dot14(); - transform.M21 = reader.ReadF2dot14(); - transform.M22 = reader.ReadF2dot14(); + transform.M11 = reader.ReadF2Dot14(); + transform.M12 = reader.ReadF2Dot14(); + transform.M21 = reader.ReadF2Dot14(); + transform.M22 = reader.ReadF2Dot14(); } var clone = GlyphVector.DeepClone(createdGlyphs[glyphIndex]); diff --git a/src/SixLabors.Fonts/TextLayout.cs b/src/SixLabors.Fonts/TextLayout.cs index 9dc6d9c24..a192a2d9a 100644 --- a/src/SixLabors.Fonts/TextLayout.cs +++ b/src/SixLabors.Fonts/TextLayout.cs @@ -982,10 +982,9 @@ private static TextBox BreakLines( // Note: Not all glyphs in a font will have a codepoint associated with them. e.g. most compositions, ligatures, etc. CodePoint codePoint = codePointEnumerator.Current; if (isSubstituted && - metrics.Count == 1 && - glyph.FontMetrics.TryGetCodePoint(glyph.GlyphId, out CodePoint substitution)) + metrics.Count == 1) { - codePoint = substitution; + codePoint = glyph.CodePoint; } // Determine whether the glyph advance should be calculated using vertical or horizontal metrics @@ -1046,7 +1045,7 @@ VerticalOrientationType.Rotate or glyph.TextAttributes, glyph.TextDecorations, layoutMode, - options.ColorFontSupport)[0]; + options.ColorFontSupport); if (isHorizontalLayout || shouldRotate) { @@ -1135,17 +1134,33 @@ VerticalOrientationType.Rotate or // Work out the scaled metrics for the glyph. GlyphMetrics metric = metrics[i]; + + // Convert design-space units to pixels based on the target point size. + // ScaleFactor.Y represents the vertical UPEM scaling factor for this glyph. float scaleY = pointSize / metric.ScaleFactor.Y; + + // Choose which metrics table to use based on layout orientation. + // Horizontal is the default; vertical fonts use VMTX if available. IMetricsHeader metricsHeader = isHorizontalLayout || shouldRotate ? metric.FontMetrics.HorizontalMetrics : metric.FontMetrics.VerticalMetrics; + + // Ascender and descender are stored in font design units, so scale them to pixels. float ascender = metricsHeader.Ascender * scaleY; - // Match how line height is calculated for browsers. - // https://www.w3.org/TR/CSS2/visudet.html#propdef-line-height + // Match browser line-height calculation logic. + // Reference: https://www.w3.org/TR/CSS2/visudet.html#propdef-line-height + // The line height in CSS is based on a multiple of the font-size (pointSize), + // but fonts may define a custom LineHeight in their metrics that differs from UPEM. float descender = Math.Abs(metricsHeader.Descender * scaleY); float lineHeight = metric.UnitsPerEm * scaleY; - float delta = ((metricsHeader.LineHeight * scaleY) - lineHeight) * .5F; + + // The delta centers the font's line box within the CSS line box when + // LineHeight differs from the nominal font size. + // This ensures vertical centering similar to browser rendering. + float delta = ((metricsHeader.LineHeight * scaleY) - lineHeight) * 0.5F; + + // Adjust ascender and descender symmetrically by delta to preserve visual balance. ascender -= delta; descender -= delta; diff --git a/src/SixLabors.Fonts/TextMeasurer.cs b/src/SixLabors.Fonts/TextMeasurer.cs index ae9edda93..5a0b18be6 100644 --- a/src/SixLabors.Fonts/TextMeasurer.cs +++ b/src/SixLabors.Fonts/TextMeasurer.cs @@ -255,7 +255,7 @@ internal static bool TryGetCharacterAdvances(IReadOnlyList glyphLay bool hasSize = false; if (glyphLayouts.Count == 0) { - characterBounds = Array.Empty(); + characterBounds = []; return hasSize; } @@ -277,7 +277,7 @@ internal static bool TryGetCharacterSizes(IReadOnlyList glyphLayout bool hasSize = false; if (glyphLayouts.Count == 0) { - characterBounds = Array.Empty(); + characterBounds = []; return hasSize; } @@ -302,7 +302,7 @@ internal static bool TryGetCharacterBounds(IReadOnlyList glyphLayou bool hasSize = false; if (glyphLayouts.Count == 0) { - characterBounds = Array.Empty(); + characterBounds = []; return hasSize; } diff --git a/src/SixLabors.Fonts/TextOptions.cs b/src/SixLabors.Fonts/TextOptions.cs index 23065217f..f45d48a5d 100644 --- a/src/SixLabors.Fonts/TextOptions.cs +++ b/src/SixLabors.Fonts/TextOptions.cs @@ -71,7 +71,7 @@ public Font Font /// /// Gets or sets the DPI (Dots Per Inch) to render/measure the text at. /// - /// Defaults to 72. + /// Defaults to 72F. /// public float Dpi { @@ -101,7 +101,7 @@ public float Dpi /// /// Gets or sets the line spacing. Applied as a multiple of the line height. /// - /// Defaults to 1. + /// Defaults to 1F. /// public float LineSpacing { @@ -171,9 +171,14 @@ public float LineSpacing public KerningMode KerningMode { get; set; } /// - /// Gets or sets a value indicating whether to enable various color font formats. + /// Gets or sets the positioning mode used for rendering decorations. /// - public ColorFontSupport ColorFontSupport { get; set; } = ColorFontSupport.MicrosoftColrFormat; + public DecorationPositioningMode DecorationPositioningMode { get; set; } + + /// + /// Gets or sets the color font support options. + /// + public ColorFontSupport ColorFontSupport { get; set; } = ColorFontSupport.ColrV1 | ColorFontSupport.ColrV0 | ColorFontSupport.Svg; /// /// Gets or sets the collection of additional feature tags to apply during glyph shaping. diff --git a/src/UnicodeTrieGenerator/StateAutomation/State.cs b/src/UnicodeTrieGenerator/StateAutomation/State.cs index d769b66b4..670094b39 100644 --- a/src/UnicodeTrieGenerator/StateAutomation/State.cs +++ b/src/UnicodeTrieGenerator/StateAutomation/State.cs @@ -3,7 +3,7 @@ namespace UnicodeTrieGenerator.StateAutomation; -internal class State +internal sealed class State { public State(ICollection positions, int length) { diff --git a/tests/Images/ReferenceOutput/BreakWordEnsuresSingleCharacterPerLine__WrappingLength_1_.png b/tests/Images/ReferenceOutput/BreakWordEnsuresSingleCharacterPerLine_1.png similarity index 100% rename from tests/Images/ReferenceOutput/BreakWordEnsuresSingleCharacterPerLine__WrappingLength_1_.png rename to tests/Images/ReferenceOutput/BreakWordEnsuresSingleCharacterPerLine_1.png diff --git a/tests/Images/ReferenceOutput/CanRenderEmojiFont_With_COLRv1-.png b/tests/Images/ReferenceOutput/CanRenderEmojiFont_With_COLRv1-.png new file mode 100644 index 000000000..1d5295b9a --- /dev/null +++ b/tests/Images/ReferenceOutput/CanRenderEmojiFont_With_COLRv1-.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c19f6d172c3acd12e2b0aec61710200817a0a828fc3598a155e4e1e822a7072b +size 32240 diff --git a/tests/Images/ReferenceOutput/CanRenderEmojiFont_With_COLRv1_-G-.png b/tests/Images/ReferenceOutput/CanRenderEmojiFont_With_COLRv1_-G-.png new file mode 100644 index 000000000..0a5b52c6e --- /dev/null +++ b/tests/Images/ReferenceOutput/CanRenderEmojiFont_With_COLRv1_-G-.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:00674fe3d446b6ae8b63f3891f90bda70496477511af85ea8c84a023895b6291 +size 10711 diff --git a/tests/Images/ReferenceOutput/CanRenderEmojiFont_With_SVG-.png b/tests/Images/ReferenceOutput/CanRenderEmojiFont_With_SVG-.png new file mode 100644 index 000000000..73335f15b --- /dev/null +++ b/tests/Images/ReferenceOutput/CanRenderEmojiFont_With_SVG-.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9098468512ce3aa9249fe8ccccabce00e554022438eddcbc717fa68e0074b9a3 +size 32200 diff --git a/tests/Images/ReferenceOutput/CanRenderEmojiFont_With_SVG_-G-.png b/tests/Images/ReferenceOutput/CanRenderEmojiFont_With_SVG_-G-.png new file mode 100644 index 000000000..733cb77a6 --- /dev/null +++ b/tests/Images/ReferenceOutput/CanRenderEmojiFont_With_SVG_-G-.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4f8e6bf73d9fcdd0ef782f7517af118fd25358a41cbfb19b5d50e67dc62ae230 +size 10712 diff --git a/tests/Images/ReferenceOutput/Issue_444_A__WrappingLength_860_.png b/tests/Images/ReferenceOutput/Issue_444_A_860.png similarity index 100% rename from tests/Images/ReferenceOutput/Issue_444_A__WrappingLength_860_.png rename to tests/Images/ReferenceOutput/Issue_444_A_860.png diff --git a/tests/Images/ReferenceOutput/Issue_444_B__WrappingLength_860_.png b/tests/Images/ReferenceOutput/Issue_444_B_860.png similarity index 100% rename from tests/Images/ReferenceOutput/Issue_444_B__WrappingLength_860_.png rename to tests/Images/ReferenceOutput/Issue_444_B_860.png diff --git a/tests/Images/ReferenceOutput/Issue_444_C__WrappingLength_860_.png b/tests/Images/ReferenceOutput/Issue_444_C_860.png similarity index 100% rename from tests/Images/ReferenceOutput/Issue_444_C__WrappingLength_860_.png rename to tests/Images/ReferenceOutput/Issue_444_C_860.png diff --git a/tests/Images/ReferenceOutput/Issue_444_D__WrappingLength_860_.png b/tests/Images/ReferenceOutput/Issue_444_D_860.png similarity index 100% rename from tests/Images/ReferenceOutput/Issue_444_D__WrappingLength_860_.png rename to tests/Images/ReferenceOutput/Issue_444_D_860.png diff --git a/tests/Images/ReferenceOutput/Issue_444_E__WrappingLength_860_.png b/tests/Images/ReferenceOutput/Issue_444_E_860.png similarity index 100% rename from tests/Images/ReferenceOutput/Issue_444_E__WrappingLength_860_.png rename to tests/Images/ReferenceOutput/Issue_444_E_860.png diff --git a/tests/Images/ReferenceOutput/Issue_446_A__WrappingLength_860_.png b/tests/Images/ReferenceOutput/Issue_446_A_860.png similarity index 100% rename from tests/Images/ReferenceOutput/Issue_446_A__WrappingLength_860_.png rename to tests/Images/ReferenceOutput/Issue_446_A_860.png diff --git a/tests/Images/ReferenceOutput/Issue_446_B__WrappingLength_860_.png b/tests/Images/ReferenceOutput/Issue_446_B_860.png similarity index 100% rename from tests/Images/ReferenceOutput/Issue_446_B__WrappingLength_860_.png rename to tests/Images/ReferenceOutput/Issue_446_B_860.png diff --git a/tests/Images/ReferenceOutput/Issue_448__WrappingLength_150_.png b/tests/Images/ReferenceOutput/Issue_448_150.png similarity index 100% rename from tests/Images/ReferenceOutput/Issue_448__WrappingLength_150_.png rename to tests/Images/ReferenceOutput/Issue_448_150.png diff --git a/tests/Images/ReferenceOutput/Issue_450__WrappingLength_960_.png b/tests/Images/ReferenceOutput/Issue_450_960.png similarity index 100% rename from tests/Images/ReferenceOutput/Issue_450__WrappingLength_960_.png rename to tests/Images/ReferenceOutput/Issue_450_960.png diff --git a/tests/Images/ReferenceOutput/LineWrappingWithExplicitNewLine__WrappingLength_800_.png b/tests/Images/ReferenceOutput/LineWrappingWithExplicitNewLine_800.png similarity index 100% rename from tests/Images/ReferenceOutput/LineWrappingWithExplicitNewLine__WrappingLength_800_.png rename to tests/Images/ReferenceOutput/LineWrappingWithExplicitNewLine_800.png diff --git a/tests/Images/ReferenceOutput/LineWrappingWithImplicitNewLine__WrappingLength_800_.png b/tests/Images/ReferenceOutput/LineWrappingWithImplicitNewLine_800.png similarity index 100% rename from tests/Images/ReferenceOutput/LineWrappingWithImplicitNewLine__WrappingLength_800_.png rename to tests/Images/ReferenceOutput/LineWrappingWithImplicitNewLine_800.png diff --git a/tests/Images/ReferenceOutput/RenderingTextIncludesAllGlyphs__WrappingLength_1900_.png b/tests/Images/ReferenceOutput/RenderingTextIncludesAllGlyphs_1900.png similarity index 100% rename from tests/Images/ReferenceOutput/RenderingTextIncludesAllGlyphs__WrappingLength_1900_.png rename to tests/Images/ReferenceOutput/RenderingTextIncludesAllGlyphs_1900.png diff --git a/tests/Images/ReferenceOutput/ShouldBreakIntoTwoLinesA__WrappingLength_1000_.png b/tests/Images/ReferenceOutput/ShouldBreakIntoTwoLinesA_1000.png similarity index 100% rename from tests/Images/ReferenceOutput/ShouldBreakIntoTwoLinesA__WrappingLength_1000_.png rename to tests/Images/ReferenceOutput/ShouldBreakIntoTwoLinesA_1000.png diff --git a/tests/Images/ReferenceOutput/ShouldBreakIntoTwoLinesB__WrappingLength_100_.png b/tests/Images/ReferenceOutput/ShouldBreakIntoTwoLinesB_100.png similarity index 100% rename from tests/Images/ReferenceOutput/ShouldBreakIntoTwoLinesB__WrappingLength_100_.png rename to tests/Images/ReferenceOutput/ShouldBreakIntoTwoLinesB_100.png diff --git a/tests/Images/ReferenceOutput/ShouldMatchBrowserBreak__WrappingLength_372_.png b/tests/Images/ReferenceOutput/ShouldMatchBrowserBreak_372.png similarity index 100% rename from tests/Images/ReferenceOutput/ShouldMatchBrowserBreak__WrappingLength_372_.png rename to tests/Images/ReferenceOutput/ShouldMatchBrowserBreak_372.png diff --git a/tests/Images/ReferenceOutput/ShouldNotInsertExtraLineBreaks_2__WrappingLength_400_.png b/tests/Images/ReferenceOutput/ShouldNotInsertExtraLineBreaks_2_400.png similarity index 100% rename from tests/Images/ReferenceOutput/ShouldNotInsertExtraLineBreaks_2__WrappingLength_400_.png rename to tests/Images/ReferenceOutput/ShouldNotInsertExtraLineBreaks_2_400.png diff --git a/tests/Images/ReferenceOutput/ShouldNotInsertExtraLineBreaks__WrappingLength_400_.png b/tests/Images/ReferenceOutput/ShouldNotInsertExtraLineBreaks_400.png similarity index 100% rename from tests/Images/ReferenceOutput/ShouldNotInsertExtraLineBreaks__WrappingLength_400_.png rename to tests/Images/ReferenceOutput/ShouldNotInsertExtraLineBreaks_400.png diff --git a/tests/SixLabors.Fonts.Tests/ColorGlyphRenderer.cs b/tests/SixLabors.Fonts.Tests/ColorGlyphRenderer.cs index 303f5b975..6dd5d717a 100644 --- a/tests/SixLabors.Fonts.Tests/ColorGlyphRenderer.cs +++ b/tests/SixLabors.Fonts.Tests/ColorGlyphRenderer.cs @@ -1,11 +1,23 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.Fonts.Rendering; + namespace SixLabors.Fonts.Tests; -public class ColorGlyphRenderer : GlyphRenderer, IColorGlyphRenderer +// TODO: We massively overuse this type. +// We should refactor tests to remove it where possible. +public class ColorGlyphRenderer : GlyphRenderer { public List Colors { get; } = new List(); - public void SetColor(GlyphColor color) => this.Colors.Add(color); + public override void BeginLayer(Paint paint, FillRule fillRule, ClipQuad? clipBounds) + { + if (paint is SolidPaint solidPaint) + { + this.Colors.Add(solidPaint.Color); + } + + base.BeginLayer(paint, fillRule, clipBounds); + } } diff --git a/tests/SixLabors.Fonts.Tests/CompactFontFormatTests.cs b/tests/SixLabors.Fonts.Tests/CompactFontFormatTests.cs index 3abbaa11f..81a4554e6 100644 --- a/tests/SixLabors.Fonts.Tests/CompactFontFormatTests.cs +++ b/tests/SixLabors.Fonts.Tests/CompactFontFormatTests.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.Fonts.Rendering; + namespace SixLabors.Fonts.Tests; public class CompactFontFormatTests diff --git a/tests/SixLabors.Fonts.Tests/FontCodePointsTests.cs b/tests/SixLabors.Fonts.Tests/FontCodePointsTests.cs index 2a9fc5a96..b8342e185 100644 --- a/tests/SixLabors.Fonts.Tests/FontCodePointsTests.cs +++ b/tests/SixLabors.Fonts.Tests/FontCodePointsTests.cs @@ -10,7 +10,7 @@ public class FontCodePointsTests [Fact] public void TtfTest() { - var collection = new FontCollection(); + FontCollection collection = new FontCollection(); FontFamily family = collection.Add(TestFonts.SimpleFontFile); Font font = family.CreateFont(12); @@ -33,11 +33,8 @@ public void TtfTest() HashSet glyphIds = new(); foreach (CodePoint codePoint in codePoints) { - Assert.True(font.TryGetGlyphs(codePoint, out IReadOnlyList glyphs)); - foreach (Glyph glyph in glyphs) - { - glyphIds.Add(glyph.GlyphMetrics.GlyphId); - } + Assert.True(font.TryGetGlyphs(codePoint, out Glyph? glyph)); + glyphIds.Add(glyph.Value.GlyphMetrics.GlyphId); } // Compare with https://fontdrop.info/ @@ -47,7 +44,7 @@ public void TtfTest() [Fact] public void WoffTest() { - var collection = new FontCollection(); + FontCollection collection = new(); FontFamily family = collection.Add(TestFonts.SimpleFontFileWoff); Font font = family.CreateFont(12); @@ -70,11 +67,8 @@ public void WoffTest() HashSet glyphIds = new(); foreach (CodePoint codePoint in codePoints) { - Assert.True(font.TryGetGlyphs(codePoint, out IReadOnlyList glyphs)); - foreach (Glyph glyph in glyphs) - { - glyphIds.Add(glyph.GlyphMetrics.GlyphId); - } + Assert.True(font.TryGetGlyphs(codePoint, out Glyph? glyph)); + glyphIds.Add(glyph.Value.GlyphMetrics.GlyphId); } // Compare with https://fontdrop.info/ diff --git a/tests/SixLabors.Fonts.Tests/FontLoaderTests.cs b/tests/SixLabors.Fonts.Tests/FontLoaderTests.cs index af86164b4..f3823fc9b 100644 --- a/tests/SixLabors.Fonts.Tests/FontLoaderTests.cs +++ b/tests/SixLabors.Fonts.Tests/FontLoaderTests.cs @@ -19,13 +19,13 @@ public void Issue21_LoopDetectedLoadingGlyphs() TextDecorations.None, LayoutMode.HorizontalTopBottom, ColorFontSupport.None, - out IReadOnlyList _)); + out GlyphMetrics _)); } [Fact] public void LoadFontMetadata() { - var description = FontDescription.LoadDescription(TestFonts.SimpleFontFileData()); + FontDescription description = FontDescription.LoadDescription(TestFonts.SimpleFontFileData()); Assert.Equal("SixLaborsSampleAB regular", description.FontNameInvariantCulture); Assert.Equal("Regular", description.FontSubFamilyNameInvariantCulture); @@ -34,7 +34,7 @@ public void LoadFontMetadata() [Fact] public void LoadFontMetadataWoff() { - var description = FontDescription.LoadDescription(TestFonts.SimpleFontFileWoffData()); + FontDescription description = FontDescription.LoadDescription(TestFonts.SimpleFontFileWoffData()); Assert.Equal("SixLaborsSampleAB regular", description.FontNameInvariantCulture); Assert.Equal("Regular", description.FontSubFamilyNameInvariantCulture); @@ -45,11 +45,10 @@ public void LoadFont_WithTtfFormat() { Font font = new FontCollection().Add(TestFonts.OpenSansFile).CreateFont(12); - Assert.True(font.TryGetGlyphs(new CodePoint('A'), ColorFontSupport.None, out IReadOnlyList glyphs)); + Assert.True(font.TryGetGlyphs(new CodePoint('A'), ColorFontSupport.None, out Glyph? glyph)); - Glyph glyph = glyphs[0]; GlyphRenderer r = new(); - glyph.RenderTo(r, Vector2.Zero, Vector2.Zero, GlyphLayoutMode.Horizontal, new TextOptions(font)); + glyph.Value.RenderTo(r, 0, Vector2.Zero, Vector2.Zero, GlyphLayoutMode.Horizontal, new TextOptions(font)); Assert.Equal(37, r.ControlPoints.Count); Assert.Single(r.GlyphKeys); @@ -61,10 +60,9 @@ public void LoadFont_WithWoff1Format() { Font font = new FontCollection().Add(TestFonts.OpenSansFileWoff1).CreateFont(12); - Assert.True(font.TryGetGlyphs(new CodePoint('A'), ColorFontSupport.None, out IReadOnlyList glyphs)); - Glyph glyph = glyphs[0]; + Assert.True(font.TryGetGlyphs(new CodePoint('A'), ColorFontSupport.None, out Glyph? glyph)); GlyphRenderer r = new(); - glyph.RenderTo(r, Vector2.Zero, Vector2.Zero, GlyphLayoutMode.Horizontal, new TextOptions(font)); + glyph.Value.RenderTo(r, 0, Vector2.Zero, Vector2.Zero, GlyphLayoutMode.Horizontal, new TextOptions(font)); Assert.Equal(37, r.ControlPoints.Count); Assert.Single(r.GlyphKeys); @@ -74,7 +72,7 @@ public void LoadFont_WithWoff1Format() [Fact] public void LoadFontMetadata_WithWoff1Format() { - var description = FontDescription.LoadDescription(TestFonts.OpensSansWoff1Data()); + FontDescription description = FontDescription.LoadDescription(TestFonts.OpensSansWoff1Data()); Assert.Equal("Open Sans Regular", description.FontNameInvariantCulture); Assert.Equal("Regular", description.FontSubFamilyNameInvariantCulture); @@ -83,7 +81,7 @@ public void LoadFontMetadata_WithWoff1Format() [Fact] public void LoadFontMetadata_WithWoff2Format() { - var description = FontDescription.LoadDescription(TestFonts.OpensSansWoff2Data()); + FontDescription description = FontDescription.LoadDescription(TestFonts.OpensSansWoff2Data()); Assert.Equal("Open Sans Regular", description.FontNameInvariantCulture); Assert.Equal("Regular", description.FontSubFamilyNameInvariantCulture); @@ -94,10 +92,9 @@ public void LoadFont_WithWoff2Format() { Font font = new FontCollection().Add(TestFonts.OpensSansWoff2Data()).CreateFont(12); - Assert.True(font.TryGetGlyphs(new CodePoint('A'), ColorFontSupport.None, out IReadOnlyList glyphs)); - Glyph glyph = glyphs[0]; + Assert.True(font.TryGetGlyphs(new CodePoint('A'), ColorFontSupport.None, out Glyph? glyph)); GlyphRenderer r = new(); - glyph.RenderTo(r, Vector2.Zero, Vector2.Zero, GlyphLayoutMode.Horizontal, new TextOptions(font)); + glyph.Value.RenderTo(r, 0, Vector2.Zero, Vector2.Zero, GlyphLayoutMode.Horizontal, new TextOptions(font)); Assert.Equal(37, r.ControlPoints.Count); Assert.Single(r.GlyphKeys); @@ -112,10 +109,9 @@ public void LoadFont() Assert.Equal("SixLaborsSampleAB regular", font.FontMetrics.Description.FontNameInvariantCulture); Assert.Equal("Regular", font.FontMetrics.Description.FontSubFamilyNameInvariantCulture); - Assert.True(font.TryGetGlyphs(new CodePoint('a'), ColorFontSupport.None, out IReadOnlyList glyphs)); - Glyph glyph = glyphs[0]; + Assert.True(font.TryGetGlyphs(new CodePoint('a'), ColorFontSupport.None, out Glyph? glyph)); GlyphRenderer r = new(); - glyph.RenderTo(r, Vector2.Zero, Vector2.Zero, GlyphLayoutMode.Horizontal, new TextOptions(font)); + glyph.Value.RenderTo(r, 0, Vector2.Zero, Vector2.Zero, GlyphLayoutMode.Horizontal, new TextOptions(font)); // the test font only has characters .notdef, 'a' & 'b' defined Assert.Equal(6, r.ControlPoints.Distinct().Count()); @@ -129,10 +125,9 @@ public void LoadFontWoff() Assert.Equal("SixLaborsSampleAB regular", font.FontMetrics.Description.FontNameInvariantCulture); Assert.Equal("Regular", font.FontMetrics.Description.FontSubFamilyNameInvariantCulture); - Assert.True(font.TryGetGlyphs(new CodePoint('a'), ColorFontSupport.None, out IReadOnlyList glyphs)); - Glyph glyph = glyphs[0]; + Assert.True(font.TryGetGlyphs(new CodePoint('a'), ColorFontSupport.None, out Glyph? glyph)); GlyphRenderer r = new(); - glyph.RenderTo(r, Vector2.Zero, Vector2.Zero, GlyphLayoutMode.Horizontal, new TextOptions(font)); + glyph.Value.RenderTo(r, 0, Vector2.Zero, Vector2.Zero, GlyphLayoutMode.Horizontal, new TextOptions(font)); // the test font only has characters .notdef, 'a' & 'b' defined Assert.Equal(6, r.ControlPoints.Distinct().Count()); diff --git a/tests/SixLabors.Fonts.Tests/FontMetricsTests.cs b/tests/SixLabors.Fonts.Tests/FontMetricsTests.cs index 250f63c52..3eb208812 100644 --- a/tests/SixLabors.Fonts.Tests/FontMetricsTests.cs +++ b/tests/SixLabors.Fonts.Tests/FontMetricsTests.cs @@ -13,7 +13,7 @@ public void FontMetricsMatchesReference() { // Compared to EveryFonts TTFDump metrics // https://everythingfonts.com/ttfdump - var collection = new FontCollection(); + FontCollection collection = new(); FontFamily family = collection.Add(TestFonts.OpenSansFile); Font font = family.CreateFont(12); @@ -52,7 +52,7 @@ public void FontMetricsVerticalFontMatchesReference() { // Compared to EveryFonts TTFDump metrics // https://everythingfonts.com/ttfdump - var collection = new FontCollection(); + FontCollection collection = new(); FontFamily family = collection.Add(TestFonts.NotoSansSCThinFile); Font font = family.CreateFont(12); @@ -99,7 +99,7 @@ public void FontMetricsVerticalFontMatchesReferenceCFF() { // Compared to OpenTypeJS Font Inspector metrics // https://opentype.js.org/font-inspector.html - var collection = new FontCollection(); + FontCollection collection = new(); FontFamily family = collection.Add(TestFonts.NotoSansKRRegular); Font font = family.CreateFont(12); @@ -146,11 +146,11 @@ public void GlyphMetricsMatchesReference() { // Compared to EveryFonts TTFDump metrics // https://everythingfonts.com/ttfdump - var collection = new FontCollection(); + FontCollection collection = new(); FontFamily family = collection.Add(TestFonts.OpenSansFile); Font font = family.CreateFont(12); - var codePoint = new CodePoint('A'); + CodePoint codePoint = new('A'); Assert.True(font.FontMetrics.TryGetGlyphMetrics( codePoint, @@ -158,19 +158,18 @@ public void GlyphMetricsMatchesReference() TextDecorations.None, LayoutMode.HorizontalTopBottom, ColorFontSupport.None, - out IReadOnlyList metrics)); - GlyphMetrics glyphMetrics = metrics[0]; - - Assert.Equal(codePoint, glyphMetrics.CodePoint); - Assert.Equal(font.FontMetrics.UnitsPerEm, glyphMetrics.UnitsPerEm); - Assert.Equal(new Vector2(glyphMetrics.UnitsPerEm * 72F), glyphMetrics.ScaleFactor); - Assert.Equal(1295, glyphMetrics.AdvanceWidth); - Assert.Equal(2789, glyphMetrics.AdvanceHeight); - Assert.Equal(1293, glyphMetrics.Width); - Assert.Equal(1468, glyphMetrics.Height); - Assert.Equal(0, glyphMetrics.LeftSideBearing); - Assert.Equal(721, glyphMetrics.TopSideBearing); - Assert.Equal(GlyphType.Standard, glyphMetrics.GlyphType); + out GlyphMetrics metrics)); + + Assert.Equal(codePoint, metrics.CodePoint); + Assert.Equal(font.FontMetrics.UnitsPerEm, metrics.UnitsPerEm); + Assert.Equal(new Vector2(metrics.UnitsPerEm * 72F), metrics.ScaleFactor); + Assert.Equal(1295, metrics.AdvanceWidth); + Assert.Equal(2789, metrics.AdvanceHeight); + Assert.Equal(1293, metrics.Width); + Assert.Equal(1468, metrics.Height); + Assert.Equal(0, metrics.LeftSideBearing); + Assert.Equal(721, metrics.TopSideBearing); + Assert.Equal(GlyphType.Standard, metrics.GlyphType); } [Fact] @@ -178,11 +177,11 @@ public void GlyphMetricsMatchesReference_WithWoff1format() { // Compared to EveryFonts TTFDump metrics // https://everythingfonts.com/ttfdump - var collection = new FontCollection(); + FontCollection collection = new(); FontFamily family = collection.Add(TestFonts.OpenSansFileWoff1); Font font = family.CreateFont(12); - var codePoint = new CodePoint('A'); + CodePoint codePoint = new('A'); Assert.True(font.FontMetrics.TryGetGlyphMetrics( codePoint, @@ -190,19 +189,18 @@ public void GlyphMetricsMatchesReference_WithWoff1format() TextDecorations.None, LayoutMode.HorizontalTopBottom, ColorFontSupport.None, - out IReadOnlyList metrics)); - GlyphMetrics glyphMetrics = metrics[0]; - - Assert.Equal(codePoint, glyphMetrics.CodePoint); - Assert.Equal(font.FontMetrics.UnitsPerEm, glyphMetrics.UnitsPerEm); - Assert.Equal(new Vector2(glyphMetrics.UnitsPerEm * 72F), glyphMetrics.ScaleFactor); - Assert.Equal(1295, glyphMetrics.AdvanceWidth); - Assert.Equal(2789, glyphMetrics.AdvanceHeight); - Assert.Equal(1293, glyphMetrics.Width); - Assert.Equal(1468, glyphMetrics.Height); - Assert.Equal(0, glyphMetrics.LeftSideBearing); - Assert.Equal(721, glyphMetrics.TopSideBearing); - Assert.Equal(GlyphType.Standard, glyphMetrics.GlyphType); + out GlyphMetrics metrics)); + + Assert.Equal(codePoint, metrics.CodePoint); + Assert.Equal(font.FontMetrics.UnitsPerEm, metrics.UnitsPerEm); + Assert.Equal(new Vector2(metrics.UnitsPerEm * 72F), metrics.ScaleFactor); + Assert.Equal(1295, metrics.AdvanceWidth); + Assert.Equal(2789, metrics.AdvanceHeight); + Assert.Equal(1293, metrics.Width); + Assert.Equal(1468, metrics.Height); + Assert.Equal(0, metrics.LeftSideBearing); + Assert.Equal(721, metrics.TopSideBearing); + Assert.Equal(GlyphType.Standard, metrics.GlyphType); } [Fact] @@ -210,11 +208,11 @@ public void GlyphMetricsMatchesReference_WithWoff2format() { // Compared to EveryFonts TTFDump metrics // https://everythingfonts.com/ttfdump - var collection = new FontCollection(); + FontCollection collection = new(); FontFamily family = collection.Add(TestFonts.OpenSansFileWoff2); Font font = family.CreateFont(12); - var codePoint = new CodePoint('A'); + CodePoint codePoint = new('A'); Assert.True(font.FontMetrics.TryGetGlyphMetrics( codePoint, @@ -222,19 +220,18 @@ public void GlyphMetricsMatchesReference_WithWoff2format() TextDecorations.None, LayoutMode.HorizontalTopBottom, ColorFontSupport.None, - out IReadOnlyList metrics)); - GlyphMetrics glyphMetrics = metrics[0]; - - Assert.Equal(codePoint, glyphMetrics.CodePoint); - Assert.Equal(font.FontMetrics.UnitsPerEm, glyphMetrics.UnitsPerEm); - Assert.Equal(new Vector2(glyphMetrics.UnitsPerEm * 72F), glyphMetrics.ScaleFactor); - Assert.Equal(1295, glyphMetrics.AdvanceWidth); - Assert.Equal(2789, glyphMetrics.AdvanceHeight); - Assert.Equal(1293, glyphMetrics.Width); - Assert.Equal(1468, glyphMetrics.Height); - Assert.Equal(0, glyphMetrics.LeftSideBearing); - Assert.Equal(721, glyphMetrics.TopSideBearing); - Assert.Equal(GlyphType.Standard, glyphMetrics.GlyphType); + out GlyphMetrics metrics)); + + Assert.Equal(codePoint, metrics.CodePoint); + Assert.Equal(font.FontMetrics.UnitsPerEm, metrics.UnitsPerEm); + Assert.Equal(new Vector2(metrics.UnitsPerEm * 72F), metrics.ScaleFactor); + Assert.Equal(1295, metrics.AdvanceWidth); + Assert.Equal(2789, metrics.AdvanceHeight); + Assert.Equal(1293, metrics.Width); + Assert.Equal(1468, metrics.Height); + Assert.Equal(0, metrics.LeftSideBearing); + Assert.Equal(721, metrics.TopSideBearing); + Assert.Equal(GlyphType.Standard, metrics.GlyphType); } [Fact] @@ -242,11 +239,11 @@ public void GlyphMetricsVerticalMatchesReference() { // Compared to EveryFonts TTFDump metrics // https://everythingfonts.com/ttfdump - var collection = new FontCollection(); + FontCollection collection = new(); FontFamily family = collection.Add(TestFonts.NotoSansSCThinFile); Font font = family.CreateFont(12); - var codePoint = new CodePoint('A'); + CodePoint codePoint = new('A'); Assert.True(font.FontMetrics.TryGetGlyphMetrics( codePoint, @@ -254,19 +251,18 @@ public void GlyphMetricsVerticalMatchesReference() TextDecorations.None, LayoutMode.HorizontalTopBottom, ColorFontSupport.None, - out IReadOnlyList metrics)); - GlyphMetrics glyphMetrics = metrics[0]; + out GlyphMetrics metrics)); // Position 0. - Assert.Equal(codePoint, glyphMetrics.CodePoint); - Assert.Equal(font.FontMetrics.UnitsPerEm, glyphMetrics.UnitsPerEm); - Assert.Equal(new Vector2(glyphMetrics.UnitsPerEm * 72F), glyphMetrics.ScaleFactor); - Assert.Equal(364, glyphMetrics.AdvanceWidth); - Assert.Equal(1000, glyphMetrics.AdvanceHeight); - Assert.Equal(265, glyphMetrics.Width); - Assert.Equal(666, glyphMetrics.Height); - Assert.Equal(33, glyphMetrics.LeftSideBearing); - Assert.Equal(134, glyphMetrics.TopSideBearing); - Assert.Equal(GlyphType.Fallback, glyphMetrics.GlyphType); + Assert.Equal(codePoint, metrics.CodePoint); + Assert.Equal(font.FontMetrics.UnitsPerEm, metrics.UnitsPerEm); + Assert.Equal(new Vector2(metrics.UnitsPerEm * 72F), metrics.ScaleFactor); + Assert.Equal(364, metrics.AdvanceWidth); + Assert.Equal(1000, metrics.AdvanceHeight); + Assert.Equal(265, metrics.Width); + Assert.Equal(666, metrics.Height); + Assert.Equal(33, metrics.LeftSideBearing); + Assert.Equal(134, metrics.TopSideBearing); + Assert.Equal(GlyphType.Fallback, metrics.GlyphType); } } diff --git a/tests/SixLabors.Fonts.Tests/Fonts/NotoColorEmoji-Regular.ttf b/tests/SixLabors.Fonts.Tests/Fonts/NotoColorEmoji-Regular.ttf new file mode 100644 index 000000000..5d7a86f3c Binary files /dev/null and b/tests/SixLabors.Fonts.Tests/Fonts/NotoColorEmoji-Regular.ttf differ diff --git a/tests/SixLabors.Fonts.Tests/GlyphRenderer.cs b/tests/SixLabors.Fonts.Tests/GlyphRenderer.cs index b3804129f..38dec39da 100644 --- a/tests/SixLabors.Fonts.Tests/GlyphRenderer.cs +++ b/tests/SixLabors.Fonts.Tests/GlyphRenderer.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Numerics; +using SixLabors.Fonts.Rendering; namespace SixLabors.Fonts.Tests; @@ -18,9 +19,9 @@ public class GlyphRenderer : IGlyphRenderer public List GlyphKeys { get; } = new(); - public bool BeginGlyph(in FontRectangle rect, in GlyphRendererParameters parameters) + public bool BeginGlyph(in FontRectangle bounds, in GlyphRendererParameters parameters) { - this.GlyphRects.Add(rect); + this.GlyphRects.Add(bounds); this.GlyphKeys.Add(this.parameters = parameters); return true; } @@ -55,6 +56,12 @@ public void MoveTo(Vector2 point) this.ControlPointsOnCurve.Add(point); } + public void ArcTo(float radiusX, float radiusY, float rotation, bool largeArc, bool sweep, Vector2 point) + { + this.ControlPoints.Add(point); + this.ControlPointsOnCurve.Add(point); + } + public void QuadraticBezierTo(Vector2 secondControlPoint, Vector2 point) { this.ControlPoints.Add(secondControlPoint); @@ -66,7 +73,7 @@ public void EndText() { } - public void BeginText(in FontRectangle rect) + public void BeginText(in FontRectangle bounds) { } @@ -76,4 +83,12 @@ public TextDecorations EnabledDecorations() public void SetDecoration(TextDecorations textDecorations, Vector2 start, Vector2 end, float thickness) { } + + public virtual void BeginLayer(Paint paint, FillRule fillRule, ClipQuad? clipBounds) + { + } + + public void EndLayer() + { + } } diff --git a/tests/SixLabors.Fonts.Tests/GlyphTests.cs b/tests/SixLabors.Fonts.Tests/GlyphTests.cs index ebec89c08..a827f94f4 100644 --- a/tests/SixLabors.Fonts.Tests/GlyphTests.cs +++ b/tests/SixLabors.Fonts.Tests/GlyphTests.cs @@ -4,6 +4,7 @@ using System.Globalization; using System.Numerics; using Moq; +using SixLabors.Fonts.Rendering; using SixLabors.Fonts.Tables.TrueType; using SixLabors.Fonts.Tables.TrueType.Glyphs; using SixLabors.Fonts.Tests.Fakes; @@ -41,35 +42,36 @@ public void RenderToPointAndSingleDPI() 0, metrics.UnitsPerEm, textRun.TextAttributes, - textRun.TextDecorations); + textRun.TextDecorations, + GlyphType.Standard); Glyph glyph = new(glyphMetrics.CloneForRendering(textRun), font.Size); Vector2 locationInFontSpace = new Vector2(99, 99) / 72; // glyph ends up 10px over due to offset in fake glyph - glyph.RenderTo(this.renderer, locationInFontSpace, Vector2.Zero, GlyphLayoutMode.Horizontal, new TextOptions(font)); + glyph.RenderTo(this.renderer, 0, locationInFontSpace, Vector2.Zero, GlyphLayoutMode.Horizontal, new TextOptions(font)); Assert.Equal(new FontRectangle(99, 89, 0, 0), this.renderer.GlyphRects.Single()); } [Fact] - public void IdenticalGlyphsInDifferentPlacesCreateIdenticalKeys() + public void IdenticalGlyphsInDifferentPlacesCreateDifferentKeys() { Font fakeFont = CreateFont("AB"); - var textRenderer = new TextRenderer(this.renderer); + TextRenderer textRenderer = new(this.renderer); textRenderer.RenderText("ABA", new TextOptions(fakeFont)); - Assert.Equal(this.renderer.GlyphKeys[0], this.renderer.GlyphKeys[2]); + Assert.NotEqual(this.renderer.GlyphKeys[0], this.renderer.GlyphKeys[2]); Assert.NotEqual(this.renderer.GlyphKeys[1], this.renderer.GlyphKeys[2]); } [Fact] public void BeginGlyph_ReturnsFalse_SkipRenderingFigures() { - var renderer = new Mock(); + Mock renderer = new(); renderer.Setup(x => x.BeginGlyph(It.Ref.IsAny, It.Ref.IsAny)).Returns(false); Font fakeFont = CreateFont("A"); - var textRenderer = new TextRenderer(renderer.Object); + TextRenderer textRenderer = new(renderer.Object); textRenderer.RenderText("ABA", new TextOptions(fakeFont)); renderer.Verify(x => x.BeginFigure(), Times.Never); @@ -78,10 +80,10 @@ public void BeginGlyph_ReturnsFalse_SkipRenderingFigures() [Fact] public void BeginGlyph_ReturnsTrue_RendersFigures() { - var renderer = new Mock(); + Mock renderer = new(); renderer.Setup(x => x.BeginGlyph(It.Ref.IsAny, It.Ref.IsAny)).Returns(true); Font fakeFont = CreateFont("A"); - var textRenderer = new TextRenderer(renderer.Object); + TextRenderer textRenderer = new(renderer.Object); textRenderer.RenderText("ABA", new TextOptions(fakeFont)); renderer.Verify(x => x.BeginFigure(), Times.Exactly(3)); @@ -89,7 +91,7 @@ public void BeginGlyph_ReturnsTrue_RendersFigures() public static Font CreateFont(string text, float pointSize = 1) { - var fc = (IFontMetricsCollection)new FontCollection(); + IFontMetricsCollection fc = new FontCollection(); Font d = fc.AddMetrics(new FakeFontInstance(text), CultureInfo.InvariantCulture).CreateFont(12); return new Font(d, pointSize); } @@ -100,42 +102,21 @@ public void LoadGlyph() Font font = new FontCollection().Add(TestFonts.SimpleFontFileData()).CreateFont(12); // Get letter A - Assert.True(font.TryGetGlyphs(new CodePoint(41), ColorFontSupport.None, out IReadOnlyList glyphs)); - Glyph g = glyphs[0]; - GlyphVector instance = ((TrueTypeGlyphMetrics)g.GlyphMetrics).GetOutline(); + Assert.True(font.TryGetGlyphs(new CodePoint(41), ColorFontSupport.None, out Glyph? glyph)); + GlyphVector instance = ((TrueTypeGlyphMetrics)glyph.Value.GlyphMetrics).GetOutline(); Assert.Equal(20, instance.ControlPoints.Count); } - [Fact] - public void RenderColrGlyph() - { - Font font = new FontCollection().Add(TestFonts.TwemojiMozillaData()).CreateFont(12); - - // Get letter Grinning Face emoji - var instance = font.FontMetrics as StreamFontMetrics; - CodePoint codePoint = this.AsCodePoint("😀"); - Assert.True(instance.TryGetGlyphId(codePoint, out ushort idx)); - IEnumerable vectors = instance.GetGlyphMetrics( - codePoint, - idx, - TextAttributes.None, - TextDecorations.None, - LayoutMode.HorizontalTopBottom, - ColorFontSupport.MicrosoftColrFormat); - - Assert.Equal(3, vectors.Count()); - } - [Fact] public void RenderColrGlyphTextRenderer() { Font font = new FontCollection().Add(TestFonts.TwemojiMozillaData()).CreateFont(12); - var renderer = new ColorGlyphRenderer(); + ColorGlyphRenderer renderer = new(); TextRenderer.RenderTextTo(renderer, "😀", new TextOptions(font) { - ColorFontSupport = ColorFontSupport.MicrosoftColrFormat + ColorFontSupport = ColorFontSupport.ColrV0 }); Assert.Equal(3, renderer.Colors.Count); @@ -144,14 +125,20 @@ public void RenderColrGlyphTextRenderer() [Fact] public void RenderColrGlyphWithVariationSelector() { - Font font = new FontCollection().Add(TestFonts.TwemojiMozillaData()).CreateFont(12); + Font font = new FontCollection().Add(TestFonts.TwemojiMozillaData()).CreateFont(72); const string text = "\u263A\uFE0F"; // Fully-qualified sequence for emoji 'smiling face' - IReadOnlyList layout = TextLayout.GenerateLayout(text.AsSpan(), new TextOptions(font)); + + ColorGlyphRenderer renderer = new(); + TextRenderer.RenderTextTo(renderer, text, new TextOptions(font) + { + ColorFontSupport = ColorFontSupport.ColrV0 + }); // Check that no glyphs were generated by the variation selector - Assert.True(layout.All(v => v.Glyph.GlyphMetrics.CodePoint.Value == 0x263A)); - Assert.Equal(4, layout.Count); + Assert.Single(renderer.GlyphKeys); + Assert.Equal(0x263A, renderer.GlyphKeys[0].CodePoint.Value); + Assert.Equal(4, renderer.Colors.Count); } [Fact] @@ -165,8 +152,8 @@ public void EmojiWidthIsComputedCorrectlyWithSubstitutionOnZwj() FontRectangle size = TextMeasurer.MeasureSize(text, new TextOptions(font)); FontRectangle size2 = TextMeasurer.MeasureSize(text2, new TextOptions(font)); - Assert.Equal(50.625F, size.Width, Comparer); - Assert.Equal(50.625F, size2.Width, Comparer); + Assert.Equal(55.652F, size.Width, Comparer); + Assert.Equal(55.617F, size2.Width, Comparer); } [Theory] @@ -180,19 +167,19 @@ public void RenderWoffGlyphs_IsEqualToTtfGlyphs(bool applyKerning, bool applyHin Font fontWoff = new FontCollection().Add(TestFonts.OpenSansFileWoff1).CreateFont(12); string testStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; - var rendererTtf = new ColorGlyphRenderer(); + ColorGlyphRenderer rendererTtf = new(); TextRenderer.RenderTextTo(rendererTtf, testStr, new TextOptions(fontTtf) { KerningMode = applyKerning ? KerningMode.Standard : KerningMode.None, HintingMode = applyHinting ? HintingMode.Standard : HintingMode.None, - ColorFontSupport = ColorFontSupport.MicrosoftColrFormat + ColorFontSupport = ColorFontSupport.ColrV0 }); - var rendererWoff = new ColorGlyphRenderer(); + ColorGlyphRenderer rendererWoff = new(); TextRenderer.RenderTextTo(rendererWoff, testStr, new TextOptions(fontWoff) { KerningMode = applyKerning ? KerningMode.Standard : KerningMode.None, HintingMode = applyHinting ? HintingMode.Standard : HintingMode.None, - ColorFontSupport = ColorFontSupport.MicrosoftColrFormat + ColorFontSupport = ColorFontSupport.ColrV0 }); Assert.Equal(expectedControlPoint, rendererWoff.ControlPoints.Count); @@ -210,17 +197,17 @@ public void RenderWoff_CompositeGlyphs_IsEqualToTtfGlyphs(string testStr) Font fontTtf = new FontCollection().Add(TestFonts.OpenSansFile).CreateFont(12); Font fontWoff = new FontCollection().Add(TestFonts.OpenSansFileWoff1).CreateFont(12); - var rendererTtf = new ColorGlyphRenderer(); + ColorGlyphRenderer rendererTtf = new(); TextRenderer.RenderTextTo(rendererTtf, testStr, new TextOptions(fontTtf) { HintingMode = HintingMode.Standard, - ColorFontSupport = ColorFontSupport.MicrosoftColrFormat + ColorFontSupport = ColorFontSupport.ColrV0 }); - var rendererWoff = new ColorGlyphRenderer(); + ColorGlyphRenderer rendererWoff = new(); TextRenderer.RenderTextTo(rendererWoff, testStr, new TextOptions(fontWoff) { HintingMode = HintingMode.Standard, - ColorFontSupport = ColorFontSupport.MicrosoftColrFormat + ColorFontSupport = ColorFontSupport.ColrV0 }); Assert.True(rendererTtf.ControlPoints.Count > 0); @@ -238,19 +225,19 @@ public void RenderWoff2Glyphs_IsEqualToTtfGlyphs(bool applyKerning, bool applyHi Font fontWoff2 = new FontCollection().Add(TestFonts.OpenSansFileWoff2).CreateFont(12); string testStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; - var rendererTtf = new ColorGlyphRenderer(); + ColorGlyphRenderer rendererTtf = new(); TextRenderer.RenderTextTo(rendererTtf, testStr, new TextOptions(fontTtf) { KerningMode = applyKerning ? KerningMode.Standard : KerningMode.None, HintingMode = applyHinting ? HintingMode.Standard : HintingMode.None, - ColorFontSupport = ColorFontSupport.MicrosoftColrFormat + ColorFontSupport = ColorFontSupport.ColrV0 }); - var rendererWoff2 = new ColorGlyphRenderer(); + ColorGlyphRenderer rendererWoff2 = new(); TextRenderer.RenderTextTo(rendererWoff2, testStr, new TextOptions(fontWoff2) { KerningMode = applyKerning ? KerningMode.Standard : KerningMode.None, HintingMode = applyHinting ? HintingMode.Standard : HintingMode.None, - ColorFontSupport = ColorFontSupport.MicrosoftColrFormat + ColorFontSupport = ColorFontSupport.ColrV0 }); Assert.Equal(expectedControlPoints, rendererWoff2.ControlPoints.Count); @@ -268,17 +255,17 @@ public void RenderWoff2_CompositeGlyphs_IsEqualToTtfGlyphs(string testStr) Font fontTtf = new FontCollection().Add(TestFonts.OpenSansFile).CreateFont(12); Font fontWoff2 = new FontCollection().Add(TestFonts.OpenSansFileWoff2).CreateFont(12); - var rendererTtf = new ColorGlyphRenderer(); + ColorGlyphRenderer rendererTtf = new(); TextRenderer.RenderTextTo(rendererTtf, testStr, new TextOptions(fontTtf) { HintingMode = HintingMode.Standard, - ColorFontSupport = ColorFontSupport.MicrosoftColrFormat + ColorFontSupport = ColorFontSupport.ColrV0 }); - var rendererWoff2 = new ColorGlyphRenderer(); + ColorGlyphRenderer rendererWoff2 = new(); TextRenderer.RenderTextTo(rendererWoff2, testStr, new TextOptions(fontWoff2) { HintingMode = HintingMode.Standard, - ColorFontSupport = ColorFontSupport.MicrosoftColrFormat + ColorFontSupport = ColorFontSupport.ColrV0 }); Assert.True(rendererTtf.ControlPoints.Count > 0); diff --git a/tests/SixLabors.Fonts.Tests/ImageComparison/TolerantImageComparer.cs b/tests/SixLabors.Fonts.Tests/ImageComparison/TolerantImageComparer.cs index 2cf4ff9fa..58ae66e5c 100644 --- a/tests/SixLabors.Fonts.Tests/ImageComparison/TolerantImageComparer.cs +++ b/tests/SixLabors.Fonts.Tests/ImageComparison/TolerantImageComparer.cs @@ -58,7 +58,7 @@ public TolerantImageComparer(float imageThreshold, int perPixelManhattanThreshol public override ImageSimilarityReport CompareImagesOrFrames(int index, ImageFrame expected, ImageFrame actual) { - if (expected.Size() != actual.Size()) + if (expected.Size != actual.Size) { throw new InvalidOperationException("Calling ImageComparer is invalid when dimensions mismatch!"); } diff --git a/tests/SixLabors.Fonts.Tests/Issues/Issues_191.cs b/tests/SixLabors.Fonts.Tests/Issues/Issues_191.cs index 794190ac3..9e99274a6 100644 --- a/tests/SixLabors.Fonts.Tests/Issues/Issues_191.cs +++ b/tests/SixLabors.Fonts.Tests/Issues/Issues_191.cs @@ -16,20 +16,15 @@ public void CanLoadMacintoshGlyphs() const ColorFontSupport support = ColorFontSupport.None; - Assert.True(font.TryGetGlyphs(new CodePoint('A'), support, out IReadOnlyList glyphsA)); - Glyph[] a = glyphsA.ToArray(); + Assert.True(font.TryGetGlyphs(new CodePoint('A'), support, out Glyph? ga)); + Assert.True(font.TryGetGlyphs(new CodePoint('x'), support, out Glyph? gx)); - Assert.True(font.TryGetGlyphs(new CodePoint('x'), support, out IReadOnlyList glyphsX)); - Glyph[] x = glyphsX.ToArray(); - - Glyph ga = Assert.Single(a); - Glyph gx = Assert.Single(x); Assert.NotEqual(ga, gx); - Assert.Equal(1366, ga.GlyphMetrics.AdvanceWidth); - Assert.Equal(2048, ga.GlyphMetrics.AdvanceHeight); + Assert.Equal(1366, ga.Value.GlyphMetrics.AdvanceWidth); + Assert.Equal(2048, ga.Value.GlyphMetrics.AdvanceHeight); - Assert.Equal(1024, gx.GlyphMetrics.AdvanceWidth); - Assert.Equal(2048, gx.GlyphMetrics.AdvanceHeight); + Assert.Equal(1024, gx.Value.GlyphMetrics.AdvanceWidth); + Assert.Equal(2048, gx.Value.GlyphMetrics.AdvanceHeight); } } diff --git a/tests/SixLabors.Fonts.Tests/Issues/Issues_23.cs b/tests/SixLabors.Fonts.Tests/Issues/Issues_23.cs index 63e605d10..bf465fc29 100644 --- a/tests/SixLabors.Fonts.Tests/Issues/Issues_23.cs +++ b/tests/SixLabors.Fonts.Tests/Issues/Issues_23.cs @@ -1,17 +1,19 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.Fonts.Rendering; + namespace SixLabors.Fonts.Tests.Issues; public class Issues_23 { [Fact] - public void BleadingFonts() + public void BleedingFonts() { // wendy one returns wrong points for 'o' Font font = new FontCollection().Add(TestFonts.WendyOneFile).CreateFont(12); - var r = new GlyphRenderer(); + GlyphRenderer r = new(); new TextRenderer(r).RenderText("o", new TextOptions(new Font(font, 30))); diff --git a/tests/SixLabors.Fonts.Tests/Issues/Issues_334.cs b/tests/SixLabors.Fonts.Tests/Issues/Issues_334.cs index 905c90539..446f4615f 100644 --- a/tests/SixLabors.Fonts.Tests/Issues/Issues_334.cs +++ b/tests/SixLabors.Fonts.Tests/Issues/Issues_334.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.Fonts.Rendering; + namespace SixLabors.Fonts.Tests.Issues; public class Issues_334 diff --git a/tests/SixLabors.Fonts.Tests/Issues/Issues_337.cs b/tests/SixLabors.Fonts.Tests/Issues/Issues_337.cs index cf6e611d1..e93dcb16a 100644 --- a/tests/SixLabors.Fonts.Tests/Issues/Issues_337.cs +++ b/tests/SixLabors.Fonts.Tests/Issues/Issues_337.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.Fonts.Rendering; + namespace SixLabors.Fonts.Tests.Issues; public class Issues_337 diff --git a/tests/SixLabors.Fonts.Tests/Issues/Issues_383.cs b/tests/SixLabors.Fonts.Tests/Issues/Issues_383.cs index 73ff6e826..683a87576 100644 --- a/tests/SixLabors.Fonts.Tests/Issues/Issues_383.cs +++ b/tests/SixLabors.Fonts.Tests/Issues/Issues_383.cs @@ -3,6 +3,7 @@ #if OS_WINDOWS using System.Numerics; +using SixLabors.Fonts.Rendering; namespace SixLabors.Fonts.Tests.Issues; @@ -21,31 +22,31 @@ public void CanBreakLinesWithShortWrappingLength() }; // OK - TextRenderer.RenderTextTo(new DummyGlyphRenderer(), "i", textOption); + TextRenderer.RenderTextTo(new NoOpGlyphRenderer(), "i", textOption); // OK - TextRenderer.RenderTextTo(new DummyGlyphRenderer(), "v", textOption); + TextRenderer.RenderTextTo(new NoOpGlyphRenderer(), "v", textOption); // raise ArgumentOutOfRangeException - TextRenderer.RenderTextTo(new DummyGlyphRenderer(), "a", textOption); + TextRenderer.RenderTextTo(new NoOpGlyphRenderer(), "a", textOption); textOption.WrappingLength = 9.0F; // OK - TextRenderer.RenderTextTo(new DummyGlyphRenderer(), "i", textOption); + TextRenderer.RenderTextTo(new NoOpGlyphRenderer(), "i", textOption); // raise ArgumentOutOfRangeException - TextRenderer.RenderTextTo(new DummyGlyphRenderer(), "v", textOption); + TextRenderer.RenderTextTo(new NoOpGlyphRenderer(), "v", textOption); // OK - TextRenderer.RenderTextTo(new DummyGlyphRenderer(), "i\r\nv", textOption); + TextRenderer.RenderTextTo(new NoOpGlyphRenderer(), "i\r\nv", textOption); // raise ArgumentOutOfRangeException - TextRenderer.RenderTextTo(new DummyGlyphRenderer(), "v\r\ni", textOption); + TextRenderer.RenderTextTo(new NoOpGlyphRenderer(), "v\r\ni", textOption); } } -internal class DummyGlyphRenderer : IGlyphRenderer +internal class NoOpGlyphRenderer : IGlyphRenderer { public void BeginFigure() { @@ -83,6 +84,10 @@ public void MoveTo(Vector2 point) { } + public void ArcTo(float radiusX, float radiusY, float rotation, bool largeArc, bool sweep, Vector2 point) + { + } + public void QuadraticBezierTo(Vector2 secondControlPoint, Vector2 point) { } @@ -90,5 +95,13 @@ public void QuadraticBezierTo(Vector2 secondControlPoint, Vector2 point) public void SetDecoration(TextDecorations textDecorations, Vector2 start, Vector2 end, float thickness) { } + + public void BeginLayer(Paint paint, FillRule fillRule, ClipQuad? clipBounds) + { + } + + public void EndLayer() + { + } } #endif diff --git a/tests/SixLabors.Fonts.Tests/Issues/Issues_39.cs b/tests/SixLabors.Fonts.Tests/Issues/Issues_39.cs index 060851d06..ce2b56531 100644 --- a/tests/SixLabors.Fonts.Tests/Issues/Issues_39.cs +++ b/tests/SixLabors.Fonts.Tests/Issues/Issues_39.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Globalization; +using SixLabors.Fonts.Rendering; using SixLabors.Fonts.Tests.Fakes; namespace SixLabors.Fonts.Tests.Issues; @@ -13,14 +14,13 @@ public void RenderingEmptyString_DoesNotThrow() { Font font = CreateFont("\t x"); - var r = new GlyphRenderer(); - + GlyphRenderer r = new(); new TextRenderer(r).RenderText(string.Empty, new TextOptions(new Font(font, 30))); } public static Font CreateFont(string text) { - var fc = (IFontMetricsCollection)new FontCollection(); + IFontMetricsCollection fc = (IFontMetricsCollection)new FontCollection(); Font d = fc.AddMetrics(new FakeFontInstance(text), CultureInfo.InvariantCulture).CreateFont(12); return new Font(d, 1F); } diff --git a/tests/SixLabors.Fonts.Tests/Issues/Issues_417.cs b/tests/SixLabors.Fonts.Tests/Issues/Issues_417.cs index a43f57d06..0d12f914f 100644 --- a/tests/SixLabors.Fonts.Tests/Issues/Issues_417.cs +++ b/tests/SixLabors.Fonts.Tests/Issues/Issues_417.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.Fonts.Rendering; + namespace SixLabors.Fonts.Tests.Issues; public class Issues_417 diff --git a/tests/SixLabors.Fonts.Tests/Issues/Issues_462.cs b/tests/SixLabors.Fonts.Tests/Issues/Issues_462.cs new file mode 100644 index 000000000..8317477fb --- /dev/null +++ b/tests/SixLabors.Fonts.Tests/Issues/Issues_462.cs @@ -0,0 +1,79 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.Fonts.Rendering; +using SixLabors.Fonts.Unicode; + +namespace SixLabors.Fonts.Tests.Issues; + +public class Issues_462 +{ + private readonly FontFamily emoji = new FontCollection().Add(TestFonts.NotoColorEmojiRegular); + private readonly FontFamily noto = new FontCollection().Add(TestFonts.NotoSansRegular); + + [Fact] + public void CanRenderEmojiFont_With_COLRv1() + { + Font font = this.emoji.CreateFont(100); + const string text = "a😨 b😅\r\nc🥲 d🤩"; + + TextOptions options = new(font) + { + ColorFontSupport = ColorFontSupport.ColrV1, + LineSpacing = 1.8F, + FallbackFontFamilies = new[] { this.noto }, + TextRuns = new List + { + new() + { + Start = 0, + End = text.GetGraphemeCount(), + TextDecorations = TextDecorations.Strikeout | TextDecorations.Underline | TextDecorations.Overline + } + } + }; + + GlyphRenderer renderer = new(); + TextRenderer.RenderTextTo(renderer, text, options); + Assert.Equal(10, renderer.GlyphKeys.Count); + + // There are too many metrics to validate here so we just ensure no exceptions are thrown + // and the rendering looks correct by inspecting the snapshot. + TextLayoutTestUtilities.TestLayout( + text, + options, + includeGeometry: true); + } + + [Fact] + public void CanRenderEmojiFont_With_SVG() + { + Font font = this.emoji.CreateFont(100); + const string text = "a😨 b😅\r\nc🥲 d🤩"; + + TextOptions options = new(font) + { + ColorFontSupport = ColorFontSupport.Svg, + LineSpacing = 1.8F, + FallbackFontFamilies = new[] { this.noto }, + TextRuns = new List + { + new() + { + Start = 0, + End = text.GetGraphemeCount(), + TextDecorations = TextDecorations.Strikeout | TextDecorations.Underline | TextDecorations.Overline + } + } + }; + + GlyphRenderer renderer = new(); + TextRenderer.RenderTextTo(renderer, text, options); + Assert.Equal(10, renderer.GlyphKeys.Count); + + TextLayoutTestUtilities.TestLayout( + text, + options, + includeGeometry: true); + } +} diff --git a/tests/SixLabors.Fonts.Tests/SixLabors.Fonts.Tests.csproj b/tests/SixLabors.Fonts.Tests/SixLabors.Fonts.Tests.csproj index c88bf9e20..74a10e685 100644 --- a/tests/SixLabors.Fonts.Tests/SixLabors.Fonts.Tests.csproj +++ b/tests/SixLabors.Fonts.Tests/SixLabors.Fonts.Tests.csproj @@ -1,4 +1,4 @@ - + True @@ -29,11 +29,11 @@ Comment out this constant declaration to disable all tests based upon image generation. This allows us to make breaking changes to the Fonts API without breaking the tests. --> - $(DefineConstants);SUPPORTS_DRAWING - true + - + @@ -48,7 +48,7 @@ - + diff --git a/tests/SixLabors.Fonts.Tests/SystemFontCollectionTests.cs b/tests/SixLabors.Fonts.Tests/SystemFontCollectionTests.cs index bc9a3fb79..c58627abe 100644 --- a/tests/SixLabors.Fonts.Tests/SystemFontCollectionTests.cs +++ b/tests/SixLabors.Fonts.Tests/SystemFontCollectionTests.cs @@ -34,9 +34,9 @@ public void SystemFonts_CanGetFont_ByCulture() FontFamily family = SystemFonts.Families.First(); Assert.False(family == default); - Assert.Equal(family, SystemFonts.Get(family.Name, family.Culture)); + Assert.Equal(family, SystemFonts.GetByCulture(family.Name, family.Culture)); - SystemFonts.TryGet(family.Name, family.Culture, out FontFamily family2); + SystemFonts.TryGetByCulture(family.Name, family.Culture, out FontFamily family2); Assert.Equal(family, family2); Assert.Contains(family, SystemFonts.GetByCulture(family.Culture)); diff --git a/tests/SixLabors.Fonts.Tests/Tables/AdvancedTypographic/GPos/GPosTableTests.cs b/tests/SixLabors.Fonts.Tests/Tables/AdvancedTypographic/GPos/GPosTableTests.cs index 641727edf..3c20a2318 100644 --- a/tests/SixLabors.Fonts.Tests/Tables/AdvancedTypographic/GPos/GPosTableTests.cs +++ b/tests/SixLabors.Fonts.Tests/Tables/AdvancedTypographic/GPos/GPosTableTests.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.Fonts.Rendering; + namespace SixLabors.Fonts.Tests.Tables.AdvancedTypographic.GPos; public class GPosTableTests diff --git a/tests/SixLabors.Fonts.Tests/Tables/AdvancedTypographic/GSub/GSubTableTests.Indic.cs b/tests/SixLabors.Fonts.Tests/Tables/AdvancedTypographic/GSub/GSubTableTests.Indic.cs index 500dca3dd..d2dc8b2ca 100644 --- a/tests/SixLabors.Fonts.Tests/Tables/AdvancedTypographic/GSub/GSubTableTests.Indic.cs +++ b/tests/SixLabors.Fonts.Tests/Tables/AdvancedTypographic/GSub/GSubTableTests.Indic.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.Fonts.Rendering; + namespace SixLabors.Fonts.Tests.Tables.AdvancedTypographic.GSub; /// diff --git a/tests/SixLabors.Fonts.Tests/Tables/AdvancedTypographic/Gsub/GSubTableTests.Hangul.cs b/tests/SixLabors.Fonts.Tests/Tables/AdvancedTypographic/Gsub/GSubTableTests.Hangul.cs index a80608456..78fb7173d 100644 --- a/tests/SixLabors.Fonts.Tests/Tables/AdvancedTypographic/Gsub/GSubTableTests.Hangul.cs +++ b/tests/SixLabors.Fonts.Tests/Tables/AdvancedTypographic/Gsub/GSubTableTests.Hangul.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.Fonts.Rendering; + namespace SixLabors.Fonts.Tests.Tables.AdvancedTypographic.GSub; /// diff --git a/tests/SixLabors.Fonts.Tests/Tables/AdvancedTypographic/Gsub/GSubTableTests.Universal.cs b/tests/SixLabors.Fonts.Tests/Tables/AdvancedTypographic/Gsub/GSubTableTests.Universal.cs index ad4b9730b..8fc74efc0 100644 --- a/tests/SixLabors.Fonts.Tests/Tables/AdvancedTypographic/Gsub/GSubTableTests.Universal.cs +++ b/tests/SixLabors.Fonts.Tests/Tables/AdvancedTypographic/Gsub/GSubTableTests.Universal.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.Fonts.Rendering; + namespace SixLabors.Fonts.Tests.Tables.AdvancedTypographic.GSub; /// diff --git a/tests/SixLabors.Fonts.Tests/Tables/AdvancedTypographic/Gsub/GSubTableTests.cs b/tests/SixLabors.Fonts.Tests/Tables/AdvancedTypographic/Gsub/GSubTableTests.cs index 210482371..495cc21c2 100644 --- a/tests/SixLabors.Fonts.Tests/Tables/AdvancedTypographic/Gsub/GSubTableTests.cs +++ b/tests/SixLabors.Fonts.Tests/Tables/AdvancedTypographic/Gsub/GSubTableTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.Fonts.Rendering; using SixLabors.Fonts.Tables.AdvancedTypographic; namespace SixLabors.Fonts.Tests.Tables.AdvancedTypographic.GSub; diff --git a/tests/SixLabors.Fonts.Tests/TestFonts.cs b/tests/SixLabors.Fonts.Tests/TestFonts.cs index b7a5e2330..5b4cfecb2 100644 --- a/tests/SixLabors.Fonts.Tests/TestFonts.cs +++ b/tests/SixLabors.Fonts.Tests/TestFonts.cs @@ -263,6 +263,8 @@ public static class TestFonts public static string VeryBerryProRegular => GetFullPath("VeryBerryProRegular.ttf"); + public static string NotoColorEmojiRegular => GetFullPath("NotoColorEmoji-Regular.ttf"); + public static Stream TwemojiMozillaData() => OpenStream(TwemojiMozillaFile); public static Stream SegoeuiEmojiData() => OpenStream(SegoeuiEmojiFile); diff --git a/tests/SixLabors.Fonts.Tests/TextLayoutTestUtilities.cs b/tests/SixLabors.Fonts.Tests/TextLayoutTestUtilities.cs index 51c680305..df83e8a49 100644 --- a/tests/SixLabors.Fonts.Tests/TextLayoutTestUtilities.cs +++ b/tests/SixLabors.Fonts.Tests/TextLayoutTestUtilities.cs @@ -7,7 +7,9 @@ using SixLabors.Fonts.Tables.AdvancedTypographic; using SixLabors.Fonts.Tests.TestUtilities; using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing; using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Drawing.Shapes.Text; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; #endif @@ -20,6 +22,7 @@ public static void TestLayout( string text, TextOptions options, float percentageTolerance = 0.05F, + bool includeGeometry = false, [CallerMemberName] string test = "", params object[] properties) { @@ -36,11 +39,18 @@ public static void TestLayout( int imageWidth = isVertical ? width : Math.Max(width, wrappingLength + 1); int imageHeight = isVertical ? Math.Max(height, wrappingLength + 1) : height; - using Image img = new(imageWidth, imageHeight, Color.White); + List extended = properties?.ToList() ?? new(); + if (options.WrappingLength > 0) + { + extended.Insert(0, options.WrappingLength); + } + + // First render the text using the rich text renderer. + using Image img = new(Configuration.Default, imageWidth, imageHeight, Color.White.ToPixel()); img.Mutate(ctx => ctx.DrawText(FromTextOptions(options), text, Color.Black)); - if (wrappingLength > 0) + if (options.WrappingLength > 0) { if (!options.LayoutMode.IsHorizontal()) { @@ -50,23 +60,38 @@ public static void TestLayout( { img.Mutate(x => x.DrawLine(Color.Red, 1, new(wrappingLength, 0), new(wrappingLength, height))); } + } + + img.DebugSave("png", test, properties: extended.ToArray()); + img.CompareToReference(percentageTolerance: percentageTolerance, test: test, properties: extended.ToArray()); + + if (!includeGeometry) + { + return; + } - if (properties.Any()) + // Now render the text using geometry-only renderer. + extended.Insert(0, "G"); + using Image img2 = new(Configuration.Default, imageWidth, imageHeight, Color.White.ToPixel()); + + IReadOnlyList glyphs = TextBuilder.GenerateGlyphs2(text, options); + + img2.Mutate(ctx => ctx.Fill(Color.Black, glyphs)); + + if (options.WrappingLength > 0) + { + if (!options.LayoutMode.IsHorizontal()) { - List extended = properties.ToList(); - extended.Insert(0, options.WrappingLength); - img.CompareToReference(percentageTolerance: percentageTolerance, test: test, properties: extended.ToArray()); + img2.Mutate(x => x.DrawLine(Color.Red, 1, new(0, wrappingLength), new(width, wrappingLength))); } else { - img.CompareToReference(percentageTolerance: percentageTolerance, test: test, properties: new { options.WrappingLength }); + img2.Mutate(x => x.DrawLine(Color.Red, 1, new(wrappingLength, 0), new(wrappingLength, height))); } } - else - { - img.CompareToReference(percentageTolerance: percentageTolerance, test: test, properties: properties); - } + img2.DebugSave("png", test, properties: extended.ToArray()); + img2.CompareToReference(percentageTolerance: percentageTolerance, test: test, properties: extended.ToArray()); #endif } @@ -90,6 +115,7 @@ private static RichTextOptions FromTextOptions(TextOptions options) VerticalAlignment = options.VerticalAlignment, LayoutMode = options.LayoutMode, KerningMode = options.KerningMode, + DecorationPositioningMode = options.DecorationPositioningMode, ColorFontSupport = options.ColorFontSupport, FeatureTags = new List(options.FeatureTags), }; @@ -105,9 +131,14 @@ private static RichTextOptions FromTextOptions(TextOptions options) Start = run.Start, End = run.End, TextAttributes = run.TextAttributes, - TextDecorations = run.TextDecorations + TextDecorations = run.TextDecorations, + StrikeoutPen = new SolidPen(Color.Green, 11.3334F), + UnderlinePen = new SolidPen(Color.Blue, 15.5555F), + OverlinePen = new SolidPen(Color.Purple, 13.7777F) }); } + + result.TextRuns = runs; } return result; diff --git a/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs b/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs index 7b3ff0ca3..41cc7e54d 100644 --- a/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs +++ b/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs @@ -3,10 +3,9 @@ using System.Globalization; using System.Numerics; -using System.Text; +using SixLabors.Fonts.Rendering; using SixLabors.Fonts.Tests.Fakes; using SixLabors.Fonts.Unicode; -using SixLabors.ImageSharp.Drawing.Processing; namespace SixLabors.Fonts.Tests; @@ -48,10 +47,8 @@ public void CanDetectVerticalMixedLayoutMode(LayoutMode mode, bool vertical) public void FakeFontGetGlyph() { Font font = CreateFont("hello world"); - - Assert.True(font.TryGetGlyphs(new CodePoint('h'), ColorFontSupport.None, out IReadOnlyList glyphs)); - Glyph glyph = glyphs[0]; - Assert.NotEqual(default, glyph); + Assert.True(font.TryGetGlyphs(new CodePoint('h'), ColorFontSupport.None, out Glyph? glyph)); + Assert.NotNull(glyph); } [Theory] @@ -555,7 +552,7 @@ public void CountLinesWithSpan() public void CountLinesWrappingLength(string text, int wrappingLength, int usedLines) { Font font = CreateRenderingFont(); - RichTextOptions options = new(font) + TextOptions options = new(font) { WrappingLength = wrappingLength }; @@ -1233,24 +1230,72 @@ public static List GenerateGlyphsBoxes(string text, TextOptions o renderer.RenderText(text, options); return glyphBuilder.GlyphBounds; } + public readonly List GlyphBounds = new(); - public CaptureGlyphBoundBuilder() { } + + public CaptureGlyphBoundBuilder() + { + } + bool IGlyphRenderer.BeginGlyph(in FontRectangle bounds, in GlyphRendererParameters parameters) { this.GlyphBounds.Add(bounds); return true; } - public void BeginFigure() { } - public void MoveTo(Vector2 point) { } - public void QuadraticBezierTo(Vector2 secondControlPoint, Vector2 point) { } - public void CubicBezierTo(Vector2 secondControlPoint, Vector2 thirdControlPoint, Vector2 point) { } - public void LineTo(Vector2 point) { } - public void EndFigure() { } - public void EndGlyph() { } - public void EndText() { } - void IGlyphRenderer.BeginText(in FontRectangle bounds) { } + + public void BeginFigure() + { + } + + public void MoveTo(Vector2 point) + { + } + + public void ArcTo(float radiusX, float radiusY, float rotation, bool largeArc, bool sweep, Vector2 point) + { + } + + public void QuadraticBezierTo(Vector2 secondControlPoint, Vector2 point) + { + } + + public void CubicBezierTo(Vector2 secondControlPoint, Vector2 thirdControlPoint, Vector2 point) + { + } + + public void LineTo(Vector2 point) + { + } + + public void EndFigure() + { + } + + public void EndGlyph() + { + } + + public void EndText() + { + } + + void IGlyphRenderer.BeginText(in FontRectangle bounds) + { + } + public TextDecorations EnabledDecorations() => TextDecorations.None; - public void SetDecoration(TextDecorations textDecorations, Vector2 start, Vector2 end, float thickness) { } + + public void SetDecoration(TextDecorations textDecorations, Vector2 start, Vector2 end, float thickness) + { + } + + public void BeginLayer(Paint paint, FillRule fillRule, ClipQuad? clipBounds) + { + } + + public void EndLayer() + { + } } private static readonly Font OpenSansTTF = new FontCollection().Add(TestFonts.OpenSansFile).CreateFont(10); diff --git a/tests/SixLabors.Fonts.Tests/TextOptionsTests.cs b/tests/SixLabors.Fonts.Tests/TextOptionsTests.cs index 8efe34a8f..52ab2cc3e 100644 --- a/tests/SixLabors.Fonts.Tests/TextOptionsTests.cs +++ b/tests/SixLabors.Fonts.Tests/TextOptionsTests.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Numerics; +using SixLabors.Fonts.Rendering; using SixLabors.Fonts.Tables.AdvancedTypographic; namespace SixLabors.Fonts.Tests; diff --git a/tests/SixLabors.Fonts.Tests/Unicode/BidiAlgorithmTests.cs b/tests/SixLabors.Fonts.Tests/Unicode/BidiAlgorithmTests.cs index 9ecdecf27..d5764783d 100644 --- a/tests/SixLabors.Fonts.Tests/Unicode/BidiAlgorithmTests.cs +++ b/tests/SixLabors.Fonts.Tests/Unicode/BidiAlgorithmTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.Fonts.Rendering; using SixLabors.Fonts.Unicode; using Xunit.Abstractions;