Skip to content

Conversation

@JimBobSquarePants
Copy link
Member

Prerequisites

  • I have written a descriptive pull-request title
  • I have verified that there are no overlapping pull-requests open
  • I have verified that I am following matches the existing coding patterns and practice as demonstrated in the repository. These follow strict Stylecop rules 👮.
  • I have provided test coverage for my change (where applicable)

Description

Fixes #462

This PR adds support for COLRv1 and SVG fonts.

A significant rewrite of the rendering API was required to allow the rendering of layers with custom composition and intersection rules.

Summary:

  • Adds support for SVG parsing and rendering
  • Adds support for COLRv1 parsing and rendering
  • Streamlines rendering of multilayer glyphs into a single metrics operation
  • Refactored the IGlyphRenderer interface to allow for rendering of layers within a multilayer glyph.
  • Adds option to render decorations using the main font metrics.

Notes:

  • COLR support for CFF fonts has temporarily been removed. This requires WIP: Compact Font Format (CFF) Version 2 #342 to be completed first to prevent merge conflicts in areas of significant complexity.
  • Render output comparison is temporarily disabled until an API compatible ImageSharp.Drawing version can be deployed.

I have a local version of ImageSharp.Drawing that supports the breaking changes. Example output is as follows.

COLRv1
CanRenderEmojiFont_With_COLRv1-

SVG
CanRenderEmojiFont_With_SVG-

@JimBobSquarePants
Copy link
Member Author

@tocsoft @brianpopow I don't actually expect you to review this as it's massive. I just thought you might like a look.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR adds comprehensive support for COLRv1 and SVG color font formats, representing a significant enhancement to the font rendering capabilities. The changes refactor the rendering API to enable multi-layer glyph rendering with custom composition and intersection rules.

Key changes:

  • Implements COLRv1 and SVG font parsing and rendering with gradient support and compositing modes
  • Refactors IGlyphRenderer interface to support layered rendering via new BeginLayer/EndLayer methods
  • Consolidates multi-layer glyph metrics into single operations for improved performance
  • Adds DecorationPositioningMode option for text decoration rendering

Reviewed Changes

Copilot reviewed 133 out of 153 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/SixLabors.Fonts/Tables/Svg/*.cs New SVG table parsing and glyph source implementation with path command conversion
src/SixLabors.Fonts/Tables/General/Colr/*.cs Complete COLRv1 table parsing with paint graph flattening and layer resolution
src/SixLabors.Fonts/Rendering/*.cs New rendering types for paints, gradients, and path commands
src/SixLabors.Fonts/StreamFontMetrics*.cs Refactored glyph metrics to support color fonts via PaintedGlyphMetrics
tests/SixLabors.Fonts.Tests/*.cs Updated tests with new namespace imports and signature changes
src/SixLabors.Fonts/Tables/General/CpalTable.cs Fixed color component order from BGR to RGB

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

}

return Span<LayerRecord>.Empty;
return [];
Copy link

Copilot AI Oct 28, 2025

Choose a reason for hiding this comment

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

[nitpick] Consider using Span<LayerRecord>.Empty instead of [] for consistency with the previous implementation and to make the intent more explicit.

Suggested change
return [];
return Span<LayerRecord>.Empty;

Copilot uses AI. Check for mistakes.
Copy link
Member Author

Choose a reason for hiding this comment

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

The future is NOW Copilot

/// <summary>
/// Canvas metadata describing the document-space coordinate system for a painted glyph.
/// </summary>
internal readonly struct PaintedCanvas
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this rather be called PaintedCanvasMetadata ?

namespace SixLabors.Fonts.Tables.General.Colr;

// Affine matrices used by PaintTransform variants
internal readonly struct Affine2x3
Copy link
Contributor

Choose a reason for hiding this comment

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

You could trimm this down to

internal readonly record struct Affine2x3(float Xx, float Yx, float Xy, float Yy, float Dx, float Dy);

Copy link
Contributor

Choose a reason for hiding this comment

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

This would give you implemented Equals methods etc.

it compiles to:

// Decompiled with JetBrains decompiler
// Type: SixLabors.Fonts.Tables.General.Colr.Affine2x3
// Assembly: SixLabors.Fonts, Version=3.0.0.0, Culture=neutral, PublicKeyToken=d998eea7b14cab13
// MVID: F8B78715-8A9B-48FD-B293-7BC7DFBD3A37
// Assembly location: /Users/stefannikolei/projects/SixLabors/Fonts/artifacts/bin/src/SixLabors.Fonts/Debug/net8.0/SixLabors.Fonts.dll
// Local variable names from /users/stefannikolei/projects/sixlabors/fonts/artifacts/bin/src/sixlabors.fonts/debug/net8.0/sixlabors.fonts.pdb
// Compiler-generated code is shown

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;

namespace SixLabors.Fonts.Tables.General.Colr;

[IsReadOnly]
[StructLayout(LayoutKind.Sequential)]
internal readonly struct Affine2x3 : IEquatable<Affine2x3>
{
  [CompilerGenerated]
  [DebuggerBrowsable(DebuggerBrowsableState.Never)]
  private readonly float <Xx>k__BackingField;
  [CompilerGenerated]
  [DebuggerBrowsable(DebuggerBrowsableState.Never)]
  private readonly float <Yx>k__BackingField;
  [CompilerGenerated]
  [DebuggerBrowsable(DebuggerBrowsableState.Never)]
  private readonly float <Xy>k__BackingField;
  [CompilerGenerated]
  [DebuggerBrowsable(DebuggerBrowsableState.Never)]
  private readonly float <Yy>k__BackingField;
  [CompilerGenerated]
  [DebuggerBrowsable(DebuggerBrowsableState.Never)]
  private readonly float <Dx>k__BackingField;
  [CompilerGenerated]
  [DebuggerBrowsable(DebuggerBrowsableState.Never)]
  private readonly float <Dy>k__BackingField;

  public Affine2x3(float Xx, float Yx, float Xy, float Yy, float Dx, float Dy)
  {
    this.<Xx>k__BackingField = Xx;
    this.<Yx>k__BackingField = Yx;
    this.<Xy>k__BackingField = Xy;
    this.<Yy>k__BackingField = Yy;
    this.<Dx>k__BackingField = Dx;
    this.<Dy>k__BackingField = Dy;
  }

  public float Xx
  {
    [CompilerGenerated] get
    {
      return this.<Xx>k__BackingField;
    }
    [CompilerGenerated] init
    {
      this.<Xx>k__BackingField = value;
    }
  }

  public float Yx
  {
    [CompilerGenerated] get
    {
      return this.<Yx>k__BackingField;
    }
    [CompilerGenerated] init
    {
      this.<Yx>k__BackingField = value;
    }
  }

  public float Xy
  {
    [CompilerGenerated] get
    {
      return this.<Xy>k__BackingField;
    }
    [CompilerGenerated] init
    {
      this.<Xy>k__BackingField = value;
    }
  }

  public float Yy
  {
    [CompilerGenerated] get
    {
      return this.<Yy>k__BackingField;
    }
    [CompilerGenerated] init
    {
      this.<Yy>k__BackingField = value;
    }
  }

  public float Dx
  {
    [CompilerGenerated] get
    {
      return this.<Dx>k__BackingField;
    }
    [CompilerGenerated] init
    {
      this.<Dx>k__BackingField = value;
    }
  }

  public float Dy
  {
    [CompilerGenerated] get
    {
      return this.<Dy>k__BackingField;
    }
    [CompilerGenerated] init
    {
      this.<Dy>k__BackingField = value;
    }
  }

  [CompilerGenerated]
  public override string ToString()
  {
    StringBuilder builder = new StringBuilder();
    builder.Append("Affine2x3");
    builder.Append(" { ");
    if (this.PrintMembers(builder))
      builder.Append(' ');
    builder.Append('}');
    return builder.ToString();
  }

  [CompilerGenerated]
  private bool PrintMembers(StringBuilder builder)
  {
    builder.Append("Xx = ");
    builder.Append(this.Xx.ToString());
    builder.Append(", Yx = ");
    builder.Append(this.Yx.ToString());
    builder.Append(", Xy = ");
    builder.Append(this.Xy.ToString());
    builder.Append(", Yy = ");
    builder.Append(this.Yy.ToString());
    builder.Append(", Dx = ");
    builder.Append(this.Dx.ToString());
    builder.Append(", Dy = ");
    builder.Append(this.Dy.ToString());
    return true;
  }

  [CompilerGenerated]
  [SpecialName]
  public static bool op_Inequality(Affine2x3 left, Affine2x3 right)
  {
    return !Affine2x3.op_Equality(left, right);
  }

  [CompilerGenerated]
  [SpecialName]
  public static bool op_Equality(Affine2x3 left, Affine2x3 right)
  {
    return left.Equals(right);
  }

  [CompilerGenerated]
  public override int GetHashCode()
  {
    return ((((EqualityComparer<float>.Default.GetHashCode(this.<Xx>k__BackingField) * -1521134295 + EqualityComparer<float>.Default.GetHashCode(this.<Yx>k__BackingField)) * -1521134295 + EqualityComparer<float>.Default.GetHashCode(this.<Xy>k__BackingField)) * -1521134295 + EqualityComparer<float>.Default.GetHashCode(this.<Yy>k__BackingField)) * -1521134295 + EqualityComparer<float>.Default.GetHashCode(this.<Dx>k__BackingField)) * -1521134295 + EqualityComparer<float>.Default.GetHashCode(this.<Dy>k__BackingField);
  }

  [CompilerGenerated]
  public override bool Equals(object obj)
  {
    return obj is Affine2x3 other && this.Equals(other);
  }

  [CompilerGenerated]
  public bool Equals(Affine2x3 other)
  {
    return EqualityComparer<float>.Default.Equals(this.<Xx>k__BackingField, other.<Xx>k__BackingField) && EqualityComparer<float>.Default.Equals(this.<Yx>k__BackingField, other.<Yx>k__BackingField) && EqualityComparer<float>.Default.Equals(this.<Xy>k__BackingField, other.<Xy>k__BackingField) && EqualityComparer<float>.Default.Equals(this.<Yy>k__BackingField, other.<Yy>k__BackingField) && EqualityComparer<float>.Default.Equals(this.<Dx>k__BackingField, other.<Dx>k__BackingField) && EqualityComparer<float>.Default.Equals(this.<Dy>k__BackingField, other.<Dy>k__BackingField);
  }

  [CompilerGenerated]
  public void Deconstruct(
    out float Xx,
    out float Yx,
    out float Xy,
    out float Yy,
    out float Dx,
    out float Dy)
  {
    Xx = this.Xx;
    Yx = this.Yx;
    Xy = this.Xy;
    Yy = this.Yy;
    Dx = this.Dx;
    Dy = this.Dy;
  }
}

When that's not needed this would be then a style question.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Unable to draw NotoColorEmoji font

3 participants