diff --git a/src/SixLabors.Fonts/Bounds.cs b/src/SixLabors.Fonts/Bounds.cs index 4128cdf95..246a442e5 100644 --- a/src/SixLabors.Fonts/Bounds.cs +++ b/src/SixLabors.Fonts/Bounds.cs @@ -81,7 +81,16 @@ public static Bounds Load(IList controlPoints) } public static Bounds Transform(in Bounds bounds, Matrix3x2 matrix) - => new(Vector2.Transform(bounds.Min, matrix), Vector2.Transform(bounds.Max, matrix)); + { + Vector2 topLeft = Vector2.Transform(new Vector2(bounds.Min.X, bounds.Max.Y), matrix); + Vector2 topRight = Vector2.Transform(bounds.Max, matrix); + Vector2 bottomLeft = Vector2.Transform(bounds.Min, matrix); + Vector2 bottomRight = Vector2.Transform(new Vector2(bounds.Max.X, bounds.Min.Y), matrix); + + Vector2 min = Vector2.Min(Vector2.Min(topLeft, topRight), Vector2.Min(bottomLeft, bottomRight)); + Vector2 max = Vector2.Max(Vector2.Max(topLeft, topRight), Vector2.Max(bottomLeft, bottomRight)); + return new Bounds(min, max); + } public override bool Equals(object? obj) => obj is Bounds bounds && this.Equals(bounds); diff --git a/src/SixLabors.Fonts/FileFontMetrics.cs b/src/SixLabors.Fonts/FileFontMetrics.cs index 313f42073..80208c7c8 100644 --- a/src/SixLabors.Fonts/FileFontMetrics.cs +++ b/src/SixLabors.Fonts/FileFontMetrics.cs @@ -19,22 +19,39 @@ namespace SixLabors.Fonts; internal sealed class FileFontMetrics : FontMetrics { private readonly Lazy fontMetrics; + private readonly FontSource source; + /// + /// Initializes a new instance of the class. + /// + /// The filesystem path to the font. public FileFontMetrics(string path) : this(path, 0) { } - public FileFontMetrics(string path, long offset) + /// + /// Initializes a new instance of the class. + /// + /// The filesystem path to the font. + /// The offset of the font within the file. + private FileFontMetrics(string path, long offset) : this(FontDescription.LoadDescription(path), path, offset) { } - internal FileFontMetrics(FontDescription description, string path, long offset) + /// + /// Initializes a new instance of the class. + /// + /// The font description. + /// The filesystem path to the font. + /// The offset of the font within the file. + private FileFontMetrics(FontDescription description, string path, long offset) { this.Description = description; this.Path = path; - this.fontMetrics = new Lazy(() => StreamFontMetrics.LoadFont(path, offset), true); + this.source = FontSource.Create(path, offset); + this.fontMetrics = new Lazy(this.LoadFont, true); } /// @@ -48,7 +65,7 @@ internal FileFontMetrics(FontDescription description, string path, long offset) /// /// Gets the underlying that this file-backed instance delegates to. /// - internal StreamFontMetrics StreamFontMetrics => this.fontMetrics.Value; + public StreamFontMetrics StreamFontMetrics => this.fontMetrics.Value; /// public override ushort UnitsPerEm => this.fontMetrics.Value.UnitsPerEm; @@ -101,6 +118,14 @@ internal FileFontMetrics(FontDescription description, string path, long offset) /// public override float ItalicAngle => this.fontMetrics.Value.ItalicAngle; + /// + public override bool TryGetTableData(Tag tag, out ReadOnlyMemory table) + => this.source.TryGetTableData(tag, out table); + + /// + public override Stream OpenStream() + => this.source.OpenStream(); + /// internal override bool TryGetGlyphId(CodePoint codePoint, out ushort glyphId) => this.fontMetrics.Value.TryGetGlyphId(codePoint, out glyphId); @@ -143,6 +168,16 @@ public override bool TryGetGlyphMetrics( [NotNullWhen(true)] out FontGlyphMetrics? metrics) => this.fontMetrics.Value.TryGetGlyphMetrics(codePoint, textAttributes, textDecorations, layoutMode, support, out metrics); + /// + public override bool TryGetGlyphMetrics( + ushort glyphId, + TextAttributes textAttributes, + TextDecorations textDecorations, + LayoutMode layoutMode, + ColorFontSupport support, + [NotNullWhen(true)] out FontGlyphMetrics? metrics) + => this.fontMetrics.Value.TryGetGlyphMetrics(glyphId, textAttributes, textDecorations, layoutMode, support, out metrics); + /// internal override FontGlyphMetrics GetGlyphMetrics( CodePoint codePoint, @@ -182,9 +217,9 @@ internal override ReadOnlySpan GetNormalizedCoordinates() => this.fontMetrics.Value.GetNormalizedCoordinates(); /// - /// Reads a from the specified stream. + /// Reads a font collection from the specified filesystem path. /// - /// The file path. + /// The filesystem path to the font collection. /// A read-only memory region containing the font metrics. public static ReadOnlyMemory LoadFontCollection(string path) { @@ -203,4 +238,10 @@ public static ReadOnlyMemory LoadFontCollection(string path) return fonts; } + + private StreamFontMetrics LoadFont() + { + using Stream stream = this.OpenStream(); + return StreamFontMetrics.LoadFont(stream, this.source); + } } diff --git a/src/SixLabors.Fonts/Font.cs b/src/SixLabors.Fonts/Font.cs index 7ae6f0922..1c197f499 100644 --- a/src/SixLabors.Fonts/Font.cs +++ b/src/SixLabors.Fonts/Font.cs @@ -14,6 +14,7 @@ namespace SixLabors.Fonts; public sealed class Font { private readonly FontVariation[] variations; + private readonly FontWeight? requestedWeight; private readonly Lazy metrics; private readonly Lazy fontName; @@ -93,6 +94,24 @@ public Font(Font prototype, params FontVariation[] variations) this.RequestedStyle = prototype.RequestedStyle; this.Size = prototype.Size; this.variations = variations; + this.requestedWeight = prototype.requestedWeight; + this.metrics = new Lazy(this.LoadInstanceInternal, true); + this.fontName = new Lazy(this.LoadFontName, true); + } + + /// + /// Initializes a new instance of the class with a numeric weight that the + /// operating-system family resolves through its normal metrics lookup. + /// + /// The font supplying family, size, style, and variation settings. + /// The requested numeric system-font weight. + private Font(Font prototype, FontWeight weight) + { + this.Family = prototype.Family; + this.RequestedStyle = prototype.RequestedStyle; + this.Size = prototype.Size; + this.variations = prototype.variations; + this.requestedWeight = weight; this.metrics = new Lazy(this.LoadInstanceInternal, true); this.fontName = new Lazy(this.LoadFontName, true); } @@ -138,6 +157,80 @@ public Font(Font prototype, params FontVariation[] variations) /// internal FontStyle RequestedStyle { get; } + /// + /// Creates a variable-font instance at the requested weight when the font exposes a + /// registered weight axis. Static fonts are returned unchanged. + /// + /// The requested weight. + /// when the weight axis was applied. + /// The variable-font instance, or this font for a static face. + internal Font WithWeight(FontWeight weight, out bool applied) + { + applied = false; + if (this.FontMetrics.TryGetVariationAxes(out ReadOnlyMemory axes)) + { + // A variable face owns its complete weight range, so apply wght before asking the + // system collection for another static face. This also preserves every other axis. + bool hasWeightAxis = false; + foreach (Tables.AdvancedTypographic.Variations.VariationAxis axis in axes.Span) + { + if (axis.Tag == KnownVariationAxes.Weight) + { + hasWeightAxis = true; + break; + } + } + + if (hasWeightAxis) + { + applied = true; + for (int i = 0; i < this.variations.Length; i++) + { + if (this.variations[i].Tag != KnownVariationAxes.Weight) + { + continue; + } + + if (this.variations[i].Value == (int)weight) + { + return this; + } + + // Font instances are immutable and can be shared by concurrent layouts. + // Recreate every variation in storage owned by the new Font so replacing wght + // cannot alter the source Font, while preserving every other configured axis. + FontVariation[] variations = new FontVariation[this.variations.Length]; + for (int variationIndex = 0; variationIndex < this.variations.Length; variationIndex++) + { + FontVariation variation = this.variations[variationIndex]; + variations[variationIndex] = new FontVariation(variation.Tag, variation.Value); + } + + variations[i] = new FontVariation(KnownVariationAxes.Weight, (int)weight); + return new Font(this, variations); + } + + // Preserve axes such as wdth or opsz while appending wght. A new array is required + // because the Font constructor retains the complete variation set for its life. + FontVariation[] weightedVariations = new FontVariation[this.variations.Length + 1]; + this.variations.CopyTo(weightedVariations, 0); + weightedVariations[^1] = new FontVariation(KnownVariationAxes.Weight, (int)weight); + + return new Font(this, weightedVariations); + } + } + + // System collections preserve the platform's family grouping. Use an exact face exposed + // by that grouping before falling back to synthetic weight on the supplied static face. + if (this.Family.TryGetMetrics(this.RequestedStyle, weight, out FontMetrics? systemMetrics) + && !ReferenceEquals(systemMetrics, this.FontMetrics)) + { + return new Font(this, weight); + } + + return this; + } + /// /// Gets the filesystem path to the font family source. /// @@ -215,6 +308,20 @@ public bool TryGetGlyphs( [NotNullWhen(true)] out Glyph? glyph) => this.TryGetGlyph(codePoint, textAttributes, TextDecorations.None, LayoutMode.HorizontalTopBottom, support, out glyph); + /// + /// Gets the glyph identifier for the given code point. + /// + /// The code point of the character. + /// + /// When this method returns, contains the glyph identifier for the given code point if the glyph + /// is found; otherwise 0. This parameter is passed uninitialized. + /// + /// + /// if the face contains a glyph for the specified code point; otherwise, . + /// + public bool TryGetGlyphId(CodePoint codePoint, out ushort glyphId) + => this.FontMetrics.TryGetGlyphId(codePoint, out glyphId); + /// /// Gets the glyph for the given codepoint. /// @@ -260,9 +367,12 @@ public bool TryGetGlyph( ColorFontSupport support, [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 FontGlyphMetrics? metrics)) + FontMetrics fontMetrics = this.FontMetrics; + if (fontMetrics.TryGetGlyphId(codePoint, out ushort glyphId)) { + TextRun textRun = new() { Start = 0, End = 1, Font = this, TextAttributes = textAttributes, TextDecorations = textDecorations }; + FontGlyphMetrics metrics = fontMetrics.GetGlyphMetrics(codePoint, glyphId, textAttributes, textDecorations, layoutMode, support); + glyph = new(metrics.CloneForRendering(textRun), this.Size); return true; } @@ -317,6 +427,7 @@ private string LoadFontName() { StreamFontMetrics s => s, FileFontMetrics f => f.StreamFontMetrics, + MemoryFontMetrics m => m.StreamFontMetrics, _ => null }; @@ -331,6 +442,15 @@ private string LoadFontName() private FontMetrics? ResolveBaseMetrics() { + // Numeric lookup belongs to FontFamily because it owns the mapping from a family request + // to a concrete face. Only system-backed families implement this overload; custom + // collections continue through the existing FontStyle lookup below. + if (this.requestedWeight.HasValue + && this.Family.TryGetMetrics(this.RequestedStyle, this.requestedWeight.Value, out FontMetrics? weightedMetrics)) + { + return weightedMetrics; + } + if (this.Family.TryGetMetrics(this.RequestedStyle, out FontMetrics? metrics)) { return metrics; diff --git a/src/SixLabors.Fonts/FontCollection.cs b/src/SixLabors.Fonts/FontCollection.cs index 5d988955d..687fd3472 100644 --- a/src/SixLabors.Fonts/FontCollection.cs +++ b/src/SixLabors.Fonts/FontCollection.cs @@ -3,7 +3,6 @@ using System.Diagnostics.CodeAnalysis; using System.Globalization; -using SixLabors.Fonts.Tables; namespace SixLabors.Fonts; @@ -13,7 +12,7 @@ namespace SixLabors.Fonts; public sealed class FontCollection : IFontCollection, IFontMetricsCollection { private readonly HashSet searchDirectories = []; - private readonly HashSet metricsCollection = []; + private readonly HashSet metricsCollection = []; /// /// Initializes a new instance of the class. @@ -143,6 +142,29 @@ FontFamily IFontMetricsCollection.AddMetrics(FontMetrics metrics, CultureInfo cu /// void IFontMetricsCollection.AddMetrics(FontMetrics metrics) + => this.AddMetrics(metrics, familyName: null); + + /// + void IFontMetricsCollection.AddMetrics(FontMetrics metrics, string familyName) + { + Guard.NotNull(familyName, nameof(familyName)); + this.AddMetrics(metrics, familyName, style: null); + } + + /// + void IFontMetricsCollection.AddMetrics(FontMetrics metrics, string familyName, FontStyle style) + { + Guard.NotNull(familyName, nameof(familyName)); + this.AddMetrics(metrics, familyName, style); + } + + /// + /// Adds the font metrics to this collection. + /// + /// The font metrics to add. + /// The explicit family name to use for collection lookups, or to use the font description. + /// The explicit style to use for collection lookups, or to use the font description. + private void AddMetrics(FontMetrics metrics, string? familyName, FontStyle? style = null) { Guard.NotNull(metrics, nameof(metrics)); @@ -153,17 +175,41 @@ void IFontMetricsCollection.AddMetrics(FontMetrics metrics) lock (this.metricsCollection) { - this.metricsCollection.Add(metrics); + this.metricsCollection.Add(new FontCollectionEntry(metrics, familyName, style)); } } /// bool IReadOnlyFontMetricsCollection.TryGetMetrics(string name, CultureInfo culture, FontStyle style, [NotNullWhen(true)] out FontMetrics? metrics) { - metrics = ((IReadOnlyFontMetricsCollection)this).GetAllMetrics(name, culture) - .FirstOrDefault(x => x.Description.Style == style); + Guard.NotNull(name, nameof(name)); + StringComparer comparer = StringComparerHelpers.GetCaseInsensitiveStringComparer(culture); + + foreach (FontCollectionEntry entry in this.metricsCollection) + { + if (entry.GetStyle() == style && comparer.Equals(entry.GetFamilyName(culture), name)) + { + metrics = entry.Metrics; + return true; + } + } + + metrics = null; + return false; + } - return metrics != null; + /// + bool IReadOnlyFontMetricsCollection.TryGetMetrics( + string name, + CultureInfo culture, + FontStyle style, + FontWeight weight, + [NotNullWhen(true)] out FontMetrics? metrics) + { + // Custom collections retain their existing variable-font or synthetic-weight behavior; + // numeric weights do not select a different static face from the collection. + metrics = null; + return false; } /// @@ -173,17 +219,26 @@ IEnumerable IReadOnlyFontMetricsCollection.GetAllMetrics(string nam StringComparer comparer = StringComparerHelpers.GetCaseInsensitiveStringComparer(culture); return this.metricsCollection - .Where(x => comparer.Equals(x.Description.FontFamily(culture), name)) + .Where(x => comparer.Equals(x.GetFamilyName(culture), name)) + .Select(x => x.Metrics) .ToArray(); } /// ReadOnlyMemory IReadOnlyFontMetricsCollection.GetAllStyles(string name, CultureInfo culture) - => ((IReadOnlyFontMetricsCollection)this).GetAllMetrics(name, culture).Select(x => x.Description.Style).ToArray(); + { + Guard.NotNull(name, nameof(name)); + StringComparer comparer = StringComparerHelpers.GetCaseInsensitiveStringComparer(culture); + + return this.metricsCollection + .Where(x => comparer.Equals(x.GetFamilyName(culture), name)) + .Select(x => x.GetStyle()) + .ToArray(); + } /// IEnumerator IReadOnlyFontMetricsCollection.GetEnumerator() - => this.metricsCollection.GetEnumerator(); + => this.metricsCollection.Select(x => x.Metrics).GetEnumerator(); internal void AddSearchDirectories(IEnumerable directories) { @@ -202,7 +257,7 @@ private FontFamily AddImpl(string path, CultureInfo culture, out FontDescription private FontFamily AddImpl(Stream stream, CultureInfo culture, out FontDescription description) { - StreamFontMetrics metrics = StreamFontMetrics.LoadFont(stream); + MemoryFontMetrics metrics = new(stream); description = metrics.Description; return ((IFontMetricsCollection)this).AddMetrics(metrics, culture); @@ -239,18 +294,16 @@ private ReadOnlyMemory AddCollectionImpl( CultureInfo culture, out ReadOnlyMemory descriptions) { - long startPos = stream.Position; - using BigEndianBinaryReader reader = new(stream, true); - TtcHeader ttcHeader = TtcHeader.Read(reader); - FontDescription[] result = new FontDescription[(int)ttcHeader.NumFonts]; - FontFamily[] installedFamilies = new FontFamily[(int)ttcHeader.NumFonts]; + ReadOnlyMemory fontMetrics = MemoryFontMetrics.LoadFontCollection(stream); + ReadOnlySpan fonts = fontMetrics.Span; + + FontDescription[] result = new FontDescription[fonts.Length]; + FontFamily[] installedFamilies = new FontFamily[fonts.Length]; int familyCount = 0; - for (int i = 0; i < ttcHeader.NumFonts; ++i) + for (int i = 0; i < fonts.Length; ++i) { - stream.Position = startPos + ttcHeader.OffsetTable[i]; - StreamFontMetrics instance = StreamFontMetrics.LoadFont(stream); - FontFamily family = ((IFontMetricsCollection)this).AddMetrics(instance, culture); - result[i] = instance.Description; + result[i] = fonts[i].Description; + FontFamily family = ((IFontMetricsCollection)this).AddMetrics(fonts[i], culture); if (!installedFamilies.AsSpan(0, familyCount).Contains(family)) { @@ -264,7 +317,7 @@ private ReadOnlyMemory AddCollectionImpl( private FontFamily[] FamiliesByCultureImpl(CultureInfo culture) => [.. this.metricsCollection - .Select(x => x.Description.FontFamily(culture)) + .Select(x => x.GetFamilyName(culture)) .Distinct() .Select(x => new FontFamily(x, this, culture))]; @@ -274,7 +327,7 @@ private bool TryGetImpl(string name, CultureInfo culture, out FontFamily family) StringComparer comparer = StringComparerHelpers.GetCaseInsensitiveStringComparer(culture); string? match = this.metricsCollection - .Select(x => x.Description.FontFamily(culture)) + .Select(x => x.GetFamilyName(culture)) .FirstOrDefault(x => comparer.Equals(name, x)); if (match != null) @@ -296,4 +349,60 @@ private FontFamily GetImpl(string name, CultureInfo culture) throw new FontFamilyNotFoundException(name, this.searchDirectories); } + + /// + /// Stores font metrics with an optional collection family name override. + /// + private readonly struct FontCollectionEntry : IEquatable + { + private readonly string? familyName; + private readonly FontStyle? style; + + /// + /// Initializes a new instance of the struct. + /// + /// The font metrics. + /// The explicit family name, or to use the font description. + /// The explicit style, or to use the font description. + public FontCollectionEntry(FontMetrics metrics, string? familyName, FontStyle? style) + { + this.Metrics = metrics; + this.familyName = familyName; + this.style = style; + } + + /// + /// Gets the font metrics. + /// + public FontMetrics Metrics { get; } + + /// + /// Gets the family name to use for collection lookups. + /// + /// The culture used to read font-description family names. + /// The collection family name. + public string GetFamilyName(CultureInfo culture) + => this.familyName ?? this.Metrics.Description.FontFamily(culture); + + /// + /// Gets the style to use for collection lookups. + /// + /// The collection style. + public FontStyle GetStyle() + => this.style ?? this.Metrics.Description.Style; + + /// + public bool Equals(FontCollectionEntry other) + => EqualityComparer.Default.Equals(this.Metrics, other.Metrics) + && StringComparer.Ordinal.Equals(this.familyName, other.familyName) + && this.style == other.style; + + /// + public override bool Equals(object? obj) + => obj is FontCollectionEntry entry && this.Equals(entry); + + /// + public override int GetHashCode() + => HashCode.Combine(this.Metrics, this.familyName, this.style); + } } diff --git a/src/SixLabors.Fonts/FontCollectionExtensions.cs b/src/SixLabors.Fonts/FontCollectionExtensions.cs index 4da0e40ad..28627f779 100644 --- a/src/SixLabors.Fonts/FontCollectionExtensions.cs +++ b/src/SixLabors.Fonts/FontCollectionExtensions.cs @@ -15,11 +15,9 @@ public static class FontCollectionExtensions /// The containing the system fonts. public static FontCollection AddSystemFonts(this FontCollection collection) { - // This cast is safe because our underlying SystemFontCollection implements - // both interfaces separately. - foreach (FontMetrics metric in (IReadOnlyFontMetricsCollection)SystemFonts.Collection) + foreach (SystemFontFamilyMetrics metric in ((SystemFontCollection)SystemFonts.Collection).GetAllFamilyMetrics()) { - ((IFontMetricsCollection)collection).AddMetrics(metric); + ((IFontMetricsCollection)collection).AddMetrics(metric.Metrics, metric.FamilyName, metric.Style); } collection.AddSearchDirectories(SystemFonts.Collection.SearchDirectories); @@ -36,13 +34,13 @@ public static FontCollection AddSystemFonts(this FontCollection collection) public static FontCollection AddSystemFonts(this FontCollection collection, Predicate match) { bool isMatch = false; - foreach (FontMetrics metric in (IReadOnlyFontMetricsCollection)SystemFonts.Collection) + foreach (SystemFontFamilyMetrics metric in ((SystemFontCollection)SystemFonts.Collection).GetAllFamilyMetrics()) { - bool currentMatch = match(metric); + bool currentMatch = match(metric.Metrics); isMatch |= currentMatch; if (currentMatch) { - ((IFontMetricsCollection)collection).AddMetrics(metric); + ((IFontMetricsCollection)collection).AddMetrics(metric.Metrics, metric.FamilyName, metric.Style); } } diff --git a/src/SixLabors.Fonts/FontDescription.cs b/src/SixLabors.Fonts/FontDescription.cs index c1c19248e..66fb89eb3 100644 --- a/src/SixLabors.Fonts/FontDescription.cs +++ b/src/SixLabors.Fonts/FontDescription.cs @@ -26,6 +26,9 @@ internal FontDescription(NameTable nameTable, OS2Table? os2, HeadTable? head) { this.nameTable = nameTable; this.Style = ConvertStyle(os2, head); + this.Weight = os2 is not null + ? (FontWeight)os2.WeightClass + : (this.Style & FontStyle.Bold) == FontStyle.Bold ? FontWeight.Bold : FontWeight.Normal; this.FontNameInvariantCulture = this.FontName(CultureInfo.InvariantCulture); this.FontFamilyInvariantCulture = this.FontFamily(CultureInfo.InvariantCulture); @@ -37,6 +40,11 @@ internal FontDescription(NameTable nameTable, OS2Table? os2, HeadTable? head) /// public FontStyle Style { get; } + /// + /// Gets the visual weight declared by the font. + /// + public FontWeight Weight { get; } + /// /// Gets the name of the font in the invariant culture. /// diff --git a/src/SixLabors.Fonts/FontFamily.cs b/src/SixLabors.Fonts/FontFamily.cs index 94d72f0a6..ff3afe9b6 100644 --- a/src/SixLabors.Fonts/FontFamily.cs +++ b/src/SixLabors.Fonts/FontFamily.cs @@ -207,6 +207,26 @@ public readonly bool TryGetMetrics(FontStyle style, [NotNullWhen(true)] out Font return this.collection.TryGetMetrics(this.Name, this.Culture, style, out metrics); } + /// + /// Attempts to get font metrics matching the specified style and numeric weight. + /// + /// The requested font style. + /// The requested font weight. + /// The matching font metrics, if found. + /// when the family contains a matching face; otherwise, . + public readonly bool TryGetMetrics( + FontStyle style, + FontWeight weight, + [NotNullWhen(true)] out FontMetrics? metrics) + { + if (this == default) + { + FontsThrowHelper.ThrowDefaultInstance(); + } + + return this.collection.TryGetMetrics(this.Name, this.Culture, style, weight, out metrics); + } + /// public override bool Equals(object? obj) => obj is FontFamily family && this.Equals(family); diff --git a/src/SixLabors.Fonts/FontGlyphMetrics.cs b/src/SixLabors.Fonts/FontGlyphMetrics.cs index 4d794e4f9..688938039 100644 --- a/src/SixLabors.Fonts/FontGlyphMetrics.cs +++ b/src/SixLabors.Fonts/FontGlyphMetrics.cs @@ -14,22 +14,35 @@ namespace SixLabors.Fonts; /// public abstract class FontGlyphMetrics { + /// + /// The number of typographic points per inch. Scaled pixels-per-em values are computed as + /// point size multiplied by dpi, so dividing by this constant converts them to the em size + /// in device pixels. + /// + private const float PointsPerInch = 72F; + + /// + /// Negates the y-axis to convert between the y-up font coordinate space and the y-down + /// device coordinate space. + /// private static readonly Vector2 YInverter = new(1, -1); /// /// The horizontal shear applied to synthesize an oblique (faux italic) slant when an italic - /// style is requested but the resolved font face provides no italic face. This equals the - /// tangent of a 14 degree slant, matching the default oblique angle used by web browsers. + /// style is requested but the resolved font face provides no italic face. Chromium and + /// SkParagraph use an exact quarter shear, which produces an angle of approximately 14.036 + /// degrees. /// - private const float SyntheticObliqueSkew = 0.24932839F; + private const float SyntheticObliqueSkew = 1F / 4F; /// - /// The outward outline offset, expressed as a fraction of the em size, applied to synthesize - /// a bold (faux bold) weight when a bold style is requested but the resolved font face provides - /// no bold face. The value is tuned to visually approximate a real bold weight, mirroring the - /// CSS font-synthesis: weight behavior used by web browsers. + /// The outline strength, expressed as a fraction of the em size, applied to synthesize a bold + /// (faux bold) weight when a bold style is requested but the resolved font face provides no + /// bold face. The divisor of 31 retains FreeType's dilation behavior while matching the + /// synthesized weight produced by Chromium's DirectWrite backend more closely than desktop + /// FreeType's divisor of 24 or Skia Android's divisor of 34. /// - private const float SyntheticBoldEmScale = 0.021F; + private const float SyntheticBoldEmScale = 1F / 31F; internal FontGlyphMetrics( StreamFontMetrics font, @@ -63,7 +76,10 @@ internal FontGlyphMetrics( this.GlyphType = glyphType; Vector2 offset = Vector2.Zero; - Vector2 scaleFactor = new(unitsPerEM * 72F); + + // Dividing a scaled pixels-per-em value (point size * dpi) by this factor yields the + // device pixels per font unit. + Vector2 scaleFactor = new(unitsPerEM * PointsPerInch); if ((textAttributes & TextAttributes.Subscript) == TextAttributes.Subscript) { @@ -267,7 +283,7 @@ internal void ApplyAdvance(short x, short y) /// internal float GetObliqueSkew() { - Font? font = this.TextRun?.Font; + Font? font = this.TextRun?.ResolvedFont; if (font is null) { return 0F; @@ -292,28 +308,49 @@ internal static Matrix3x2 CreateObliqueMatrix(float skew) } /// - /// Gets a value indicating whether a bold (faux bold) weight must be synthesized for this - /// glyph. This is true only when the associated text run requests a bold style that the - /// resolved font face does not itself provide, mirroring the CSS font-synthesis: weight - /// behavior used by web browsers. + /// Gets a value indicating whether a heavier weight must be synthesized for this glyph. + /// Variable fonts apply their registered weight axis instead. /// /// true if bold synthesis is required; otherwise false. internal bool ShouldSynthesizeBold() { - Font? font = this.TextRun?.Font; - if (font is null) + // Chromium deliberately leaves color emoji at their authored weight. Applying outline + // dilation to a painted glyph would also distort each independently colored layer. + if (this.GlyphType == GlyphType.Painted) + { + return false; + } + + TextRun? textRun = this.TextRun; + Font? font = textRun?.ResolvedFont; + if (font is null || (textRun!.UsesVariableWeight && ReferenceEquals(font.FontMetrics, this.FontMetrics))) { return false; } - bool requestedBold = (font.RequestedStyle & FontStyle.Bold) == FontStyle.Bold; - bool resolvedBold = (this.FontMetrics.Description.Style & FontStyle.Bold) == FontStyle.Bold; - return requestedBold && !resolvedBold; + FontWeight? requestedWeight = textRun.ResolvedFontWeight; + if (!requestedWeight.HasValue && (font.RequestedStyle & FontStyle.Bold) == FontStyle.Bold) + { + requestedWeight = FontWeight.Bold; + } + + if (!requestedWeight.HasValue) + { + return false; + } + + // CSS weight values select a face; they do not represent proportional stroke strength. + // Chromium treats 600 as the boundary between regular and bold, then applies one fixed + // synthetic-bold operation when the request and resolved static face lie on opposite + // sides of that boundary. Thus 600, 700, 800, and 900 receive identical dilation. + return (int)requestedWeight.Value >= (int)FontWeight.SemiBold + && (int)this.FontMetrics.Description.Weight < (int)FontWeight.SemiBold; } /// - /// Gets the outward outline offset, in pixels, used to synthesize a bold (faux bold) weight for - /// this glyph, or 0 when no synthesis is required. + /// Gets the total x/y outline strength, in pixels, used to synthesize a bold (faux bold) weight + /// for this glyph, or 0 when no synthesis is required. The FreeType dilation algorithm + /// halves this value internally before moving points along their lateral bisectors. /// /// The scaled point size, mapped to pixels by the caller. /// The emboldening strength in pixels. @@ -327,7 +364,7 @@ internal float GetSyntheticBoldStrength(float scaledPointSize) /// /// Steps: /// 1) Select glyph bounds (or synthesize from advances if empty). - /// 2) Apply rotation if the layout mode is vertical-rotated. + /// 2) Account for synthetic weight, oblique shear, and layout rotation. /// 3) Convert from Y-up to Y-down coordinates. /// 4) Scale and translate to device space using the specified origin. /// @@ -340,7 +377,7 @@ internal float GetSyntheticBoldStrength(float scaledPointSize) internal FontRectangle GetBoundingBox(GlyphLayoutMode mode, Vector2 origin, float scaledPointSize) { Vector2 scale = new(scaledPointSize / this.ScaleFactor.X, scaledPointSize / this.ScaleFactor.Y); - Bounds b = this.Bounds; + Bounds b = this.GetDesignBounds(); // 1) Substitute fallback bounds if the glyph has no outline. if (b.Equals(Bounds.Empty)) @@ -357,33 +394,21 @@ internal FontRectangle GetBoundingBox(GlyphLayoutMode mode, Vector2 origin, floa } } - // 2) Rotate for vertical rotated layout. + // 2) Apply synthetic weight and the shared outline transform. Vector2 offsetUp = this.Offset; - // Inflate the ink bounds by the synthetic bold offset (in font units) so that the reported - // bounds enclose the emboldened outline produced during rendering. + // FreeType halves the supplied strength before moving points. Inflating both sides by that + // half-strength preserves the nominal increase in width and height for measurement while + // the renderer applies the direction-sensitive point dilation. if (this.ShouldSynthesizeBold()) { - float inflate = SyntheticBoldEmScale * this.UnitsPerEm; + float inflate = SyntheticBoldEmScale * this.UnitsPerEm * .5F; b = new Bounds(b.Min - new Vector2(inflate), b.Max + new Vector2(inflate)); } - // Apply synthetic oblique (faux italic) shear before rotation so that the reported ink - // bounds match the sheared outline produced during rendering. - float skew = this.GetObliqueSkew(); - if (skew != 0F) - { - Matrix3x2 oblique = CreateObliqueMatrix(skew); - b = Bounds.Transform(in b, oblique); - offsetUp = Vector2.Transform(offsetUp, oblique); - } - - if (mode == GlyphLayoutMode.VerticalRotated) - { - Matrix3x2 rot = Matrix3x2.CreateRotation(-MathF.PI / 2F); - b = Bounds.Transform(in b, rot); - offsetUp = Vector2.Transform(offsetUp, rot); - } + Matrix3x2 outlineTransform = this.GetOutlineTransform(mode); + b = Bounds.Transform(in b, outlineTransform); + offsetUp = Vector2.Transform(offsetUp, outlineTransform); // 3) Flip Y to convert to device-space (Y-down). Vector2 minDown = b.Min * YInverter; @@ -404,49 +429,210 @@ internal FontRectangle GetBoundingBox(GlyphLayoutMode mode, Vector2 origin, floa return new FontRectangle(location.X, location.Y, size.X, size.Y); } + /// + /// Gets the design-space bounds used to measure this glyph's rendered geometry. + /// + /// The design-space bounds. + internal virtual Bounds GetDesignBounds() => this.Bounds; + /// /// Renders the glyph to the render surface in font units relative to a bottom left origin at (0,0) /// + /// + /// The rendering sequence for a glyph is: , the + /// outline geometry via , , + /// then any text decorations via . Decoration + /// geometry is computed before the outline is emitted so that, when skip-ink applies, the + /// outline stream can be observed as it is emitted rather than requiring a second decoding + /// pass to locate the ink. + /// /// The surface renderer. /// The index of the grapheme this glyph is part of. /// The origin used to render the glyph outline. /// The origin used to render text decorations. + /// + /// The laid-out advance for the glyph in DPI-normalized layout units, including layout-time + /// spacing such as tracking and justification. Negative components indicate no layout + /// advance is available and the glyph's own metrics apply. + /// /// The glyph layout mode to render using. - /// The options used to influence the rendering of this glyph. - internal abstract void RenderTo( + /// The text run providing the styling information for this glyph. + /// The point size used to render this glyph. + /// The pixel density used to render this glyph. + /// The hinting mode used to render this glyph. + /// The skip-ink behavior for underline and overline decorations. + /// The mode used to position text decorations. + /// The font metrics used to position text decorations. + internal virtual void RenderTo( IGlyphRenderer renderer, int graphemeIndex, Vector2 glyphOrigin, Vector2 decorationOrigin, + Vector2 layoutAdvance, + GlyphLayoutMode mode, + TextRun textRun, + float pointSize, + float dpi, + HintingMode hintingMode, + TextDecorationSkipInk textDecorationSkipInk, + DecorationPositioningMode decorationPositioningMode, + FontMetrics decorationFontMetrics) + { + // https://www.unicode.org/faq/unsup_char.html + if (ShouldSkipGlyphRendering(this.CodePoint)) + { + return; + } + + // Convert the DPI-normalized layout values to device pixels. + glyphOrigin *= dpi; + decorationOrigin *= dpi; + layoutAdvance *= dpi; + float scaledPPEM = this.GetScaledSize(pointSize, dpi); + + Matrix3x2 rotation = GetRotationMatrix(mode); + FontRectangle box = this.GetBoundingBox(mode, glyphOrigin, scaledPPEM); + GlyphRendererParameters parameters = new(this, textRun, pointSize, dpi, mode, graphemeIndex); + + if (!renderer.BeginGlyph(in box, in parameters)) + { + return; + } + + bool whitespace = UnicodeUtility.ShouldRenderWhiteSpaceOnly(this.CodePoint); + bool isVerticalLayout = mode is GlyphLayoutMode.Vertical or GlyphLayoutMode.VerticalRotated; + + // Decoration geometry depends only on font metrics and scale, so it is computed + // before the outline is emitted. This lets skip-ink observe the outline stream as it + // is emitted instead of decoding the outline a second time afterwards. + TextDecorationGeometry decorationGeometry = this.ComputeDecorationGeometry( + renderer.EnabledDecorations(), + textRun, + decorationOrigin, + mode, + rotation, + scaledPPEM, + layoutAdvance, + decorationPositioningMode, + decorationFontMetrics); + + // When skip-ink applies and a decoration band can intersect the glyph's box, tee the + // outline emission into per-band interval collectors. The collectors see exactly the + // stream the renderer receives, so the measured ink matches the rendered geometry + // (including hinting) at the cost of one extra call per outline segment. The tee and + // collectors come from a per-thread scratch, so steady-state decorated rendering does + // not allocate per glyph. + // Whitespace never contributes ink; strikethrough always crosses ink by definition. + GlyphIntersectionCollector? underlineInk = null; + GlyphIntersectionCollector? overlineInk = null; + SkipInkScratch? scratch = null; + IGlyphRenderer outlineTarget = renderer; + if (textDecorationSkipInk == TextDecorationSkipInk.Auto && !whitespace) + { + bool trackUnderline = TryGetInkBand(decorationGeometry.Underline, isVerticalLayout, in box, out float underlineBandStart, out float underlineBandEnd); + bool trackOverline = TryGetInkBand(decorationGeometry.Overline, isVerticalLayout, in box, out float overlineBandStart, out float overlineBandEnd); + if (trackUnderline || trackOverline) + { + scratch = SkipInkScratch.Rent(); + if (trackUnderline) + { + underlineInk = scratch.UnderlineCollector; + underlineInk.Reset(underlineBandStart, underlineBandEnd, isVerticalLayout); + } + + if (trackOverline) + { + overlineInk = scratch.OverlineCollector; + overlineInk.Reset(overlineBandStart, overlineBandEnd, isVerticalLayout); + } + + scratch.Tee.Reset(renderer, underlineInk, overlineInk); + outlineTarget = scratch.Tee; + } + } + + try + { + if (!whitespace) + { + this.RenderOutlineTo(outlineTarget, glyphOrigin, mode, scaledPPEM, hintingMode); + } + + renderer.EndGlyph(); + + // Emit in the fixed underline, strikethrough, overline order relied upon by renderers. + EmitDecoration(renderer, decorationGeometry.Underline, underlineInk); + EmitDecoration(renderer, decorationGeometry.Strikeout, null); + EmitDecoration(renderer, decorationGeometry.Overline, overlineInk); + } + finally + { + scratch?.Release(); + } + } + + /// + /// Renders only the glyph outline geometry to the specified renderer, without glyph or + /// decoration bookkeeping, using the same transforms as + /// . + /// + /// The surface renderer. + /// The origin used to render the glyph outline, in device pixels. + /// The glyph layout mode to render using. + /// The scaled pixels-per-em value used to scale the outline. + /// The hinting mode used to render the glyph. + internal virtual void RenderOutlineTo( + IGlyphRenderer renderer, + Vector2 glyphOrigin, GlyphLayoutMode mode, - TextOptions options); + float scaledPPEM, + HintingMode hintingMode) + { + } /// - /// 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. + /// Computes the positioned line geometry for the text decorations enabled on the current + /// glyph: underline, strikeout, and overline. /// /// 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 decorations the renderer reports as enabled. + /// The text run styling this glyph, consulted for per-decoration geometry overrides. + /// The position, in device pixels, 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, + /// + /// The laid-out advance for the glyph in device pixels, including layout-time spacing such + /// as tracking and justification. Negative components indicate no layout advance is + /// available and the glyph's own metrics apply. + /// + /// The mode used to position text decorations. + /// The font metrics used to position text decorations. + /// The positioned decoration lines; absent entries are disabled or degenerate. + private TextDecorationGeometry ComputeDecorationGeometry( + TextDecorations enabledDecorations, + TextRun textRun, Vector2 location, GlyphLayoutMode mode, Matrix3x2 transform, float scaledPPEM, - TextOptions options) + Vector2 layoutAdvance, + DecorationPositioningMode decorationPositioningMode, + FontMetrics decorationFontMetrics) { - bool perGlyph = options.DecorationPositioningMode == DecorationPositioningMode.GlyphFont; + TextDecorationGeometry geometry = default; + if (enabledDecorations == TextDecorations.None) + { + return geometry; + } + + bool perGlyph = decorationPositioningMode == DecorationPositioningMode.GlyphFont; FontMetrics fontMetrics = perGlyph ? this.FontMetrics - : options.Font.FontMetrics; + : decorationFontMetrics; // 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. @@ -467,7 +653,7 @@ protected void RenderDecorationsTo( { // 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); + scaleFactor = new(fontMetrics.UnitsPerEm * PointsPerInch); offset = Vector2.Zero; if ((this.TextAttributes & TextAttributes.Subscript) == TextAttributes.Subscript) { @@ -490,10 +676,6 @@ protected void RenderDecorationsTo( if (isVerticalLayout) { float length = mode == GlyphLayoutMode.VerticalRotated ? this.AdvanceWidth : this.AdvanceHeight; - if (length == 0) - { - return (Vector2.Zero, Vector2.Zero, 0); - } Vector2 lengthScale = new Vector2(scaledPPEM) / lengthScaleFactor; Vector2 scale = new Vector2(scaledPPEM) / scaleFactor; @@ -502,6 +684,19 @@ protected void RenderDecorationsTo( Vector2 scaledOffset = (offset + new Vector2(decoratorPosition, 0)) * scale; length *= lengthScale.Y; + + // Prefer the laid-out advance so decorations span layout-time spacing + // such as tracking and justification. + if (layoutAdvance.Y >= 0) + { + length = layoutAdvance.Y; + } + + if (length == 0) + { + return (Vector2.Zero, Vector2.Zero, 0); + } + thickness *= scale.X; Vector2 tl = new(scaledOffset.X, scaledOffset.Y); @@ -529,16 +724,25 @@ protected void RenderDecorationsTo( else { float length = this.AdvanceWidth; - if (length == 0) - { - return (Vector2.Zero, Vector2.Zero, 0); - } Vector2 lengthScale = new Vector2(scaledPPEM) / lengthScaleFactor; Vector2 scale = new Vector2(scaledPPEM) / scaleFactor; Vector2 scaledOffset = (offset + new Vector2(0, decoratorPosition)) * scale; length *= lengthScale.X; + + // Prefer the laid-out advance so decorations span layout-time spacing + // such as tracking and justification. + if (layoutAdvance.X >= 0) + { + length = layoutAdvance.X; + } + + if (length == 0) + { + return (Vector2.Zero, Vector2.Zero, 0); + } + thickness *= scale.Y; Vector2 tl = new(scaledOffset.X, scaledOffset.Y); @@ -553,38 +757,128 @@ protected void RenderDecorationsTo( } } - void SetDecoration(TextDecorations decorations, float thickness, float position) + TextDecorationLine? Capture(TextDecorations decorations, float thickness, float position) { (Vector2 start, Vector2 end, float calcThickness) = GetEnds(decorations, thickness, position); - if (calcThickness != 0) + if (calcThickness == 0) + { + return null; + } + + // A run may override the metric-derived thickness (for example when it is drawn with an + // explicit stroke width). The override is already in device pixels, so it replaces the + // scaled metric thickness directly and flows on to the skip-ink band and gap clearance, + // both of which key off the line thickness. + if (textRun.GetDecorationOptions(decorations)?.Thickness is float overrideThickness) { - renderer.SetDecoration(decorations, start, end, calcThickness); + calcThickness = overrideThickness; } + + return new TextDecorationLine(decorations, start, end, calcThickness); } - // Allow the renderer to override the decorations to attach. + // The enabled decorations come from the renderer so it can override the set to attach. // When rendering glyphs vertically we use synthesized positions based upon comparisons with Pango/browsers. // We deviate from browsers in a few ways: // - When rendering rotated glyphs and use the default values because it fits the glyphs better. // - We include the adjusted scale for subscript and superscript glyphs. // - We make no attempt to adjust the underline position along a text line to render at the same position. - TextDecorations decorations = renderer.EnabledDecorations(); bool synthesized = mode == GlyphLayoutMode.Vertical; - if ((decorations & TextDecorations.Underline) == TextDecorations.Underline) + if ((enabledDecorations & TextDecorations.Underline) == TextDecorations.Underline) { - SetDecoration(TextDecorations.Underline, fontMetrics.UnderlineThickness, synthesized ? Math.Abs(fontMetrics.UnderlinePosition) : fontMetrics.UnderlinePosition); + geometry.Underline = Capture(TextDecorations.Underline, fontMetrics.UnderlineThickness, synthesized ? Math.Abs(fontMetrics.UnderlinePosition) : fontMetrics.UnderlinePosition); } - if ((decorations & TextDecorations.Strikeout) == TextDecorations.Strikeout) + if ((enabledDecorations & TextDecorations.Strikeout) == TextDecorations.Strikeout) { - SetDecoration(TextDecorations.Strikeout, fontMetrics.StrikeoutSize, synthesized ? fontMetrics.UnitsPerEm * .5F : fontMetrics.StrikeoutPosition); + geometry.Strikeout = Capture(TextDecorations.Strikeout, fontMetrics.StrikeoutSize, synthesized ? fontMetrics.UnitsPerEm * .5F : fontMetrics.StrikeoutPosition); } - if ((decorations & TextDecorations.Overline) == TextDecorations.Overline) + if ((enabledDecorations & TextDecorations.Overline) == TextDecorations.Overline) { // There's no built in metrics for overline thickness so use underline. - SetDecoration(TextDecorations.Overline, fontMetrics.UnderlineThickness, fontMetrics.UnitsPerEm - fontMetrics.UnderlinePosition); + geometry.Overline = Capture(TextDecorations.Overline, fontMetrics.UnderlineThickness, fontMetrics.UnitsPerEm - fontMetrics.UnderlinePosition); + } + + return geometry; + } + + /// + /// Computes the cross-axis interval covered by a decoration's drawn stroke. Renderers draw + /// horizontal underlines below the decoration position and overlines above it; vertical + /// positions are already offset to the drawn side when the geometry is computed, with + /// underlines extending left of the position and overlines right. + /// + /// The positioned decoration line. + /// Whether the decoration runs vertically, banding on x instead of y. + /// The band's start and end on the cross axis, in device pixels. + private static (float Start, float End) GetDecorationBand(in TextDecorationLine line, bool isVerticalLayout) + { + float bandStart; + if (isVerticalLayout) + { + bandStart = line.Decoration == TextDecorations.Overline ? line.Start.X : line.Start.X - line.Thickness; + } + else + { + bandStart = line.Decoration == TextDecorations.Overline ? line.Start.Y - line.Thickness : line.Start.Y; + } + + return (bandStart, bandStart + line.Thickness); + } + + /// + /// Computes the ink observation band for a decoration line when the glyph's bounding box + /// can reach it; the outline is not worth observing otherwise. + /// + /// The positioned decoration line, if the decoration is enabled. + /// Whether the decoration runs vertically, banding on x instead of y. + /// The glyph's bounding box, in device pixels. + /// The band's start on the cross axis, in device pixels. + /// The band's end on the cross axis, in device pixels. + /// when ink can cross the band and it should be observed. + private static bool TryGetInkBand( + in TextDecorationLine? line, + bool isVerticalLayout, + in FontRectangle box, + out float bandStart, + out float bandEnd) + { + if (line is null) + { + bandStart = 0F; + bandEnd = 0F; + return false; } + + (bandStart, bandEnd) = GetDecorationBand(line.Value, isVerticalLayout); + return isVerticalLayout + ? box.Right >= bandStart && box.Left <= bandEnd + : box.Bottom >= bandStart && box.Top <= bandEnd; + } + + /// + /// Emits a decoration line to the renderer as the full, untrimmed line together with the + /// intervals where the glyph's ink crosses the decoration band. Skip-ink carving is deferred to + /// the renderer so that a gap can be widened by the drawn thickness and extended across the + /// boundary into an adjacent glyph, which a per-glyph carve here could not do. + /// + /// The glyph renderer that receives the decoration drawing commands. + /// The positioned decoration line, if the decoration is enabled. + /// The ink collector teed into the outline emission, if skip-ink applies to this line. + private static void EmitDecoration( + IGlyphRenderer renderer, + in TextDecorationLine? line, + GlyphIntersectionCollector? ink) + { + if (line is null) + { + return; + } + + TextDecorationLine value = line.Value; + ReadOnlyMemory intersections = ink is not null ? ink.BuildIntersectionSpan() : default; + renderer.SetDecoration(value.Decoration, value.Start, value.End, value.Thickness, intersections); } /// @@ -619,7 +913,7 @@ internal float GetScaledSize(float pointSize, float dpi) /// Gets the rotation matrix for the glyph based on the layout mode. /// /// The glyph layout mode. - /// The. + /// The rotation matrix. internal static Matrix3x2 GetRotationMatrix(GlyphLayoutMode mode) { if (mode == GlyphLayoutMode.VerticalRotated) @@ -630,4 +924,141 @@ internal static Matrix3x2 GetRotationMatrix(GlyphLayoutMode mode) return Matrix3x2.Identity; } + + /// + /// Gets the transform applied to glyph outlines, combining browser-style synthetic oblique + /// shear with the rotation required by the layout mode. + /// + /// The glyph layout mode. + /// The outline transform. + internal Matrix3x2 GetOutlineTransform(GlyphLayoutMode mode) + { + // All metrics keep outlines in the glyph's Y-up coordinate system through this transform + // and convert to device-space Y-down coordinates afterwards. Decorations use + // GetRotationMatrix directly because browser underlines are not themselves italicized. + Matrix3x2 transform = CreateObliqueMatrix(this.GetObliqueSkew()); + transform *= GetRotationMatrix(mode); + return transform; + } + + /// + /// A positioned text decoration line for the current glyph, in device pixels. + /// + private readonly struct TextDecorationLine + { + /// + /// Initializes a new instance of the struct. + /// + /// The decoration the line represents. + /// The start position of the line. + /// The end position of the line. + /// The stroke thickness of the line. + public TextDecorationLine(TextDecorations decoration, Vector2 start, Vector2 end, float thickness) + { + this.Decoration = decoration; + this.Start = start; + this.End = end; + this.Thickness = thickness; + } + + /// + /// Gets the decoration the line represents. + /// + public TextDecorations Decoration { get; } + + /// + /// Gets the start position of the line. + /// + public Vector2 Start { get; } + + /// + /// Gets the end position of the line. + /// + public Vector2 End { get; } + + /// + /// Gets the stroke thickness of the line. + /// + public float Thickness { get; } + } + + /// + /// The positioned decoration lines enabled for the current glyph. Absent entries are + /// disabled or degenerate (zero length or thickness). + /// + private struct TextDecorationGeometry + { + /// + /// Gets or sets the underline decoration line. + /// + public TextDecorationLine? Underline { get; set; } + + /// + /// Gets or sets the strikethrough decoration line. + /// + public TextDecorationLine? Strikeout { get; set; } + + /// + /// Gets or sets the overline decoration line. + /// + public TextDecorationLine? Overline { get; set; } + } + + /// + /// Pooled reusable skip-ink instruments: one outline tee and one collector per observable + /// band. Steady-state decorated rendering rents an instance per glyph from the shared + /// pool, so no per-glyph allocation occurs; the collectors and tee retain their internal + /// buffers across uses. + /// + private sealed class SkipInkScratch + { + /// + /// The shared instance pool. Sized by the default pool capacity; concurrent renders + /// beyond it fall back to transient instances that are dropped on return. + /// + private static readonly ObjectPool Pool = new(new PooledObjectPolicy()); + + /// + /// Gets the collector observing the underline band. + /// + public GlyphIntersectionCollector UnderlineCollector { get; } = new(); + + /// + /// Gets the collector observing the overline band. + /// + public GlyphIntersectionCollector OverlineCollector { get; } = new(); + + /// + /// Gets the tee that forwards outline emission while mirroring it into the collectors. + /// + public TeeGlyphRenderer Tee { get; } = new(); + + /// + /// Rents a scratch instance from the shared pool. + /// + /// The rented scratch. + public static SkipInkScratch Rent() => Pool.Get(); + + /// + /// Returns the scratch to the shared pool. + /// + public void Release() => Pool.Return(this); + + /// + /// Creates scratch instances and clears the tee's forwarding targets on return so + /// pooled instances do not keep the completed glyph's renderer reachable. + /// + private sealed class PooledObjectPolicy : IPooledObjectPolicy + { + /// + public SkipInkScratch Create() => new(); + + /// + public bool Return(SkipInkScratch obj) + { + obj.Tee.Clear(); + return true; + } + } + } } diff --git a/src/SixLabors.Fonts/FontMatch.cs b/src/SixLabors.Fonts/FontMatch.cs new file mode 100644 index 000000000..b7b3ceeba --- /dev/null +++ b/src/SixLabors.Fonts/FontMatch.cs @@ -0,0 +1,26 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts; + +/// +/// Represents a font family and style matched for a character. +/// +public readonly struct FontMatch +{ + internal FontMatch(FontFamily family, FontStyle style) + { + this.Family = family; + this.Style = style; + } + + /// + /// Gets the matched font family. + /// + public FontFamily Family { get; } + + /// + /// Gets the matched font style. + /// + public FontStyle Style { get; } +} diff --git a/src/SixLabors.Fonts/FontMetrics.cs b/src/SixLabors.Fonts/FontMetrics.cs index 892c9a7a3..7ec278a86 100644 --- a/src/SixLabors.Fonts/FontMetrics.cs +++ b/src/SixLabors.Fonts/FontMetrics.cs @@ -110,6 +110,23 @@ internal FontMetrics() /// public abstract float ItalicAngle { get; } + /// + /// Attempts to get the OpenType table data for the specified tag. + /// + /// The table tag. + /// + /// When this method returns, contains the table data if the table exists; otherwise, the default value. + /// This parameter is passed uninitialized. + /// + /// if the table exists; otherwise, . + public abstract bool TryGetTableData(Tag tag, out ReadOnlyMemory table); + + /// + /// Opens a readable stream positioned at the font face data. + /// + /// A readable stream positioned at the font face data. + public abstract Stream OpenStream(); + /// /// Gets the specified glyph id matching the codepoint. /// @@ -215,6 +232,29 @@ public abstract bool TryGetGlyphMetrics( ColorFontSupport support, [NotNullWhen(true)] out FontGlyphMetrics? metrics); + /// + /// Gets the glyph metrics for a given glyph id. + /// + /// The glyph identifier. + /// 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. + /// + /// When this method returns, contains the metrics for the given glyph id and color support if the metrics + /// are found; otherwise the default value. This parameter is passed uninitialized. + /// + /// + /// if the face contains glyph metrics for the specified glyph id; otherwise, . + /// + public abstract bool TryGetGlyphMetrics( + ushort glyphId, + TextAttributes textAttributes, + TextDecorations textDecorations, + LayoutMode layoutMode, + ColorFontSupport support, + [NotNullWhen(true)] out FontGlyphMetrics? metrics); + /// /// Gets the unicode codepoints for which a glyph exists in the font. /// diff --git a/src/SixLabors.Fonts/FontReader.cs b/src/SixLabors.Fonts/FontReader.cs index 2e75ccbd5..fe5af9b7b 100644 --- a/src/SixLabors.Fonts/FontReader.cs +++ b/src/SixLabors.Fonts/FontReader.cs @@ -5,6 +5,7 @@ using System.Diagnostics.CodeAnalysis; using System.IO.Compression; using SixLabors.Fonts.Tables; +using SixLabors.Fonts.Tables.AdvancedTypographic; using SixLabors.Fonts.Tables.Woff; namespace SixLabors.Fonts; @@ -199,6 +200,22 @@ public bool TryGetReaderAtTablePosition(string tableName, [NotNullWhen(returnVal return reader != null; } + public bool TryGetTableData(Tag tag, out ReadOnlyMemory table) + { + if (!this.TryGetReaderAtTablePosition(tag.ToString(), out BigEndianBinaryReader? reader, out TableHeader? header)) + { + table = default; + return false; + } + + using (reader) + { + table = reader.ReadBytes(checked((int)header.Length)); + } + + return true; + } + public void Dispose() { if (this.isDisposed) diff --git a/src/SixLabors.Fonts/FontSource.cs b/src/SixLabors.Fonts/FontSource.cs new file mode 100644 index 000000000..1d1f0b074 --- /dev/null +++ b/src/SixLabors.Fonts/FontSource.cs @@ -0,0 +1,105 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.Fonts.Tables.AdvancedTypographic; + +namespace SixLabors.Fonts; + +/// +/// Provides access to the source bytes for a font face. +/// +internal abstract class FontSource +{ + /// + /// Creates a source backed by a filesystem path. + /// + /// The font path. + /// The face offset. + /// The font source. + public static FontSource Create(string path, long offset) => new FileFontSource(path, offset); + + /// + /// Creates a source backed by owned memory. + /// + /// The source data. + /// The face offset. + /// The font source. + public static FontSource Create(byte[] data, long offset) => new MemoryFontSource(data, offset); + + /// + /// Attempts to get the OpenType table data for the specified tag. + /// + /// The table tag. + /// + /// When this method returns, contains the table data if the table exists; otherwise, the default value. + /// This parameter is passed uninitialized. + /// + /// if the table exists; otherwise, . + public abstract bool TryGetTableData(Tag tag, out ReadOnlyMemory table); + + /// + /// Opens a readable stream positioned at the font face data. + /// + /// A readable stream positioned at the font face data. + public abstract Stream OpenStream(); + + private sealed class FileFontSource : FontSource + { + private readonly string path; + private readonly long offset; + + public FileFontSource(string path, long offset) + { + this.path = path; + this.offset = offset; + } + + /// + public override bool TryGetTableData(Tag tag, out ReadOnlyMemory table) + { + using Stream stream = this.OpenStream(); + using FontReader reader = new(stream); + return reader.TryGetTableData(tag, out table); + } + + /// + public override Stream OpenStream() + { + FileStream stream = File.OpenRead(this.path); + stream.Position = this.offset; + + return stream; + } + } + + private sealed class MemoryFontSource : FontSource + { + private readonly byte[] data; + private readonly long offset; + + public MemoryFontSource(byte[] data, long offset) + { + this.data = data; + this.offset = offset; + } + + /// + public override bool TryGetTableData(Tag tag, out ReadOnlyMemory table) + { + using Stream stream = this.OpenStream(); + using FontReader reader = new(stream); + return reader.TryGetTableData(tag, out table); + } + + /// + public override Stream OpenStream() + { + MemoryStream stream = new(this.data, writable: false) + { + Position = this.offset + }; + + return stream; + } + } +} diff --git a/src/SixLabors.Fonts/FontVariation.cs b/src/SixLabors.Fonts/FontVariation.cs index 4659a09c4..02d78a83c 100644 --- a/src/SixLabors.Fonts/FontVariation.cs +++ b/src/SixLabors.Fonts/FontVariation.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Diagnostics; +using SixLabors.Fonts.Tables.AdvancedTypographic; namespace SixLabors.Fonts; @@ -29,6 +30,17 @@ public FontVariation(string tag, float value) throw new ArgumentException("Variation axis tag must be exactly 4 characters.", nameof(tag)); } + this.Tag = Tables.AdvancedTypographic.Tag.Parse(tag); + this.Value = value; + } + + /// + /// Initializes a new instance of the struct. + /// + /// The four-byte OpenType axis tag. + /// The axis value in design-space units. + public FontVariation(Tag tag, float value) + { this.Tag = tag; this.Value = value; } @@ -36,7 +48,7 @@ public FontVariation(string tag, float value) /// /// Gets the four-character axis tag identifying the design variation. /// - public string Tag { get; } + public Tag Tag { get; } /// /// Gets the axis value in design-space units. diff --git a/src/SixLabors.Fonts/FontWeight.cs b/src/SixLabors.Fonts/FontWeight.cs new file mode 100644 index 000000000..5e186b0a5 --- /dev/null +++ b/src/SixLabors.Fonts/FontWeight.cs @@ -0,0 +1,55 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts; + +/// +/// Identifies the visual weight of a font on the OpenType and CSS weight scale. +/// +public enum FontWeight +{ + /// + /// Thin weight (100). + /// + Thin = 100, + + /// + /// Extra-light weight (200). + /// + ExtraLight = 200, + + /// + /// Light weight (300). + /// + Light = 300, + + /// + /// Normal weight (400). + /// + Normal = 400, + + /// + /// Medium weight (500). + /// + Medium = 500, + + /// + /// Semi-bold weight (600). + /// + SemiBold = 600, + + /// + /// Bold weight (700). + /// + Bold = 700, + + /// + /// Extra-bold weight (800). + /// + ExtraBold = 800, + + /// + /// Black weight (900). + /// + Black = 900 +} diff --git a/src/SixLabors.Fonts/Glyph.cs b/src/SixLabors.Fonts/Glyph.cs index 863512f7d..88a4f8b25 100644 --- a/src/SixLabors.Fonts/Glyph.cs +++ b/src/SixLabors.Fonts/Glyph.cs @@ -41,6 +41,11 @@ public FontRectangle BoundingBox(GlyphLayoutMode mode, Vector2 glyphOrigin, floa /// The index of the grapheme this glyph is part of. /// The origin used to render the glyph outline. /// The origin used to render text decorations. + /// + /// The laid-out advance for the glyph in DPI-normalized layout units, including layout-time + /// spacing such as tracking and justification. Negative components indicate no layout + /// advance is available and the glyph's own metrics apply. + /// /// The glyph layout mode to render using. /// The options to render using. internal void RenderTo( @@ -48,7 +53,26 @@ internal void RenderTo( int graphemeIndex, Vector2 glyphOrigin, Vector2 decorationOrigin, + Vector2 layoutAdvance, GlyphLayoutMode mode, TextOptions options) - => this.GlyphMetrics.RenderTo(surface, graphemeIndex, glyphOrigin, decorationOrigin, mode, options); + { + TextRun textRun = this.GlyphMetrics.TextRun; + float pointSize = textRun.Font?.Size ?? options.Font.Size; + + this.GlyphMetrics.RenderTo( + surface, + graphemeIndex, + glyphOrigin, + decorationOrigin, + layoutAdvance, + mode, + textRun, + pointSize, + options.Dpi, + options.HintingMode, + options.TextDecorationSkipInk, + options.DecorationPositioningMode, + options.Font.FontMetrics); + } } diff --git a/src/SixLabors.Fonts/GlyphIdRenderingPlan.md b/src/SixLabors.Fonts/GlyphIdRenderingPlan.md new file mode 100644 index 000000000..e4c326527 --- /dev/null +++ b/src/SixLabors.Fonts/GlyphIdRenderingPlan.md @@ -0,0 +1,268 @@ +# Rendering glyphs by glyph id + +## Problem + +Shaped text pipelines (Avalonia's `IGlyphRunImpl`, HarfBuzz output, PDF/SVG glyph runs) +hand us **glyph ids + positions**, not characters. SixLabors.Fonts currently has **no public +way to turn a glyph id into something renderable** through `IGlyphRenderer`. + +Every public rendering entry point is text-driven: + +``` +TextRenderer.Render(text, options) + -> new TextBlock(text, options) // shaping + layout from characters + -> block.RenderTo(renderer) // walks laid-out glyphs, calls FontGlyphMetrics.RenderTo +``` + +See `Rendering/TextRenderer.cs:50`. There is no equivalent that starts from a glyph id. + +Consequence in the ImageSharp.Drawing Avalonia backend: `ImageSharpDrawingContext.DrawGlyphRun` +bails whenever the run carries only glyph ids (`Text is null`), which is the common Avalonia +path — so most real text never renders. + +## What already exists (and works per-glyph) + +- **`IGlyphRenderer`** (`Rendering/IGlyphRenderer.cs`) is already glyph-agnostic: `BeginGlyph` / + figures (`MoveTo`/`LineTo`/`QuadraticBezierTo`/`CubicBezierTo`/`ArcTo`) / `EndGlyph`, plus + `BeginLayer`/`EndLayer`/`Paint` for colour glyphs. Nothing about it requires text. + +- **`Glyph`** (`Glyph.cs`) — public readonly struct wrapping a `FontGlyphMetrics` + point size. + Has public `BoundingBox(...)` but its constructor and `RenderTo(...)` are **internal**. + +- **`FontGlyphMetrics`** (`FontGlyphMetrics.cs`) — public abstract base. Public surface already + includes `GlyphId`, `CodePoint`, advances, bounds, `ScaleFactor`, `UnitsPerEm`. The actual + render is `internal abstract RenderTo(IGlyphRenderer, int graphemeIndex, Vector2 glyphOrigin, + Vector2 decorationOrigin, GlyphLayoutMode, TextOptions)`, plus `CloneForRendering(TextRun)`. + Concrete impls: `TrueTypeGlyphMetrics`, `CffGlyphMetrics`, `PaintedGlyphMetrics`. + +- **`FontMetrics`** (`FontMetrics.cs`) — the id plumbing is present but internal: + - `internal abstract bool TryGetGlyphId(CodePoint, out ushort glyphId)` (`:125`) + - `internal abstract bool TryGetCodePoint(ushort glyphId, out CodePoint)` (`:156`) — reverse map + - `public abstract bool TryGetGlyphMetrics(CodePoint, ...)` (`:210`) — **codepoint-keyed only** + - `internal abstract FontGlyphMetrics GetGlyphMetrics(CodePoint, ushort glyphId, ...)` (`:237`) + — id-aware, but internal and still requires a codepoint. + +- **`TrueTypeGlyphMetrics.RenderTo`** (`Tables/TrueType/TrueTypeGlyphMetrics.cs:135`) is the + reference implementation. It already renders purely from glyph outline data; its only couplings + to "text" are: + - `pointSize = this.TextRun.Font?.Size ?? options.Font.Size` — needs a `TextRun` **or** `TextOptions`. + - `dpi`, `HintingMode` from `TextOptions`. + - `this.CodePoint` used for `ShouldSkipGlyphRendering` / whitespace short-circuit. + - Builds `GlyphRendererParameters(this, this.TextRun, pointSize, dpi, mode, graphemeIndex)`. + +- **`GlyphRendererParameters`** (`Rendering/GlyphRendererParameters.cs`) — passed to `BeginGlyph`. + Public fields include `GlyphId`, `CodePoint`, `TextRun`, `PointSize`, `Dpi`, `LayoutMode`, + `GraphemeIndex`. Used as a cache key by renderers, so synthesized values must be stable. + +## The gap, precisely + +1. **No public id→metrics accessor.** `GetGlyphMetrics(codePoint, glyphId, …)` is internal and + demands a codepoint a caller with only an id doesn't have. +2. **No public render entry.** `Glyph.RenderTo` / `FontGlyphMetrics.RenderTo` are internal. +3. **`RenderTo` is coupled to `TextRun` + `TextOptions`.** A bare glyph id has neither. +4. **`CodePoint` coupling.** `RenderTo` uses `this.CodePoint` for skip/whitespace checks; an id may + resolve to no codepoint. Recoverable via `TryGetCodePoint`, or treated as "render the outline". +5. **`CloneForRendering(TextRun)`.** Render-time metrics are clones bound to a text run; the id path + needs a synthesized default run. + +## Proposed design + +Recommended: **(A) public id accessors**, **(B) an internal refactor decoupling the per-glyph render +from `TextOptions`/`TextRun`**, and **(C) one id-based public render entry** that builds the run via +`GlyphOptions.CreateTextRun`. Keep `Glyph`/`FontGlyphMetrics` lean; centralize the `TextRun`/options +synthesis in one place. There is no run-level helper (see Section C). + +### A. Public id accessors + +- `FontMetrics`: + ```csharp + public abstract bool TryGetGlyphMetrics( + ushort glyphId, + TextAttributes textAttributes, + TextDecorations textDecorations, + LayoutMode layoutMode, + ColorFontSupport support, + [NotNullWhen(true)] out FontGlyphMetrics? metrics); + ``` + Default impl: recover a codepoint via the existing `TryGetCodePoint(glyphId, …)` (fall back to + `default`/`CodePoint(0)` when none), then delegate to the existing internal + `GetGlyphMetrics(codePoint, glyphId, …)`. Consider promoting `TryGetCodePoint` to public — useful + for consumers doing hit-testing / fallback. + + This stays available for callers that want **metrics/bounds without rendering** (measurement, + hit-testing). It is **not** on the render path — see below. + +- `Font.TryGetGlyph(ushort glyphId, …, out Glyph)` is **not required**. The render entry point takes + the id directly (Section C), so there is no need to expose a pre-fetched `Glyph` for rendering, nor + to make `Glyph.RenderTo` public. A `TryGetGlyph` may still be added later purely as a convenience + for measurement, but it is out of scope for enabling glyph-id rendering. + +### B. Decouple `RenderTo` from `TextRun`/`TextOptions` (internal refactor) + +Extract the body of `FontGlyphMetrics.RenderTo` so it takes explicit primitives +(`pointSize`, `dpi`, `HintingMode`, `GlyphLayoutMode`, `TextRun`) rather than the +layout-oriented `TextOptions`. The existing text path passes values from its `TextOptions`; the id +path passes values read off `GlyphOptions`. This is a real refactor across the three concrete metrics +(`TrueType`/`Cff`/`Painted`) and the text-path caller (`TextBlock.Visitors.cs:805`), so it is gated +behind tests in Phase 1. + +**Why re-plumb rather than synthesize a `TextOptions`?** Performance. The caller pools/mutates a +single `GlyphOptions` across the whole run, so manufacturing a fresh `TextOptions` per glyph would +allocate in the hot loop and defeat that pooling. Passing primitives extracted from the pooled +options keeps the per-glyph path allocation-light; the only unavoidable per-glyph allocation is the +`TextRun` from `CreateTextRun`, and the rich case needs a per-glyph `RichTextRun` regardless. + +**Codepoint is best-effort on the id path.** The metrics' `CodePoint` is recovered via +`TryGetCodePoint(glyphId, …)`, which is a lossy reverse lookup: ligatures and GSUB-substituted glyphs +— precisely what shaped runs produce — frequently map to *no* codepoint, yielding `default`. This is +acceptable because the **outline is keyed by glyph id (`vector`), not codepoint** — it renders +correctly regardless. `CodePoint` only drives the skip/whitespace short-circuits (skipped when +unknown, so we simply emit the outline) and the informational `GlyphRendererParameters.CodePoint`. +Document this as an explicit, accepted limitation rather than a bug. + +Introduce a minimal render-options type so callers don't have to build a full `TextOptions`: +```csharp +public class GlyphOptions // deliberately unsealed (see below); mirrors TextOptions +{ + public required Font Font { get; set; } + public float Dpi { get; set; } = 72; + public HintingMode HintingMode { get; set; } + public LayoutMode LayoutMode { get; set; } = LayoutMode.HorizontalTopBottom; + public ColorFontSupport ColorFontSupport { get; set; } = ColorFontSupport.None; + + // The rendering origin, mirroring TextOptions.Origin. Per-glyph, like the rest of GlyphOptions: + // the caller advances it per glyph rather than passing a separate origin argument. + public Vector2 Origin { get; set; } = Vector2.Zero; + + // The grapheme this glyph belongs to. Per-glyph state (like Origin), so it lives on the options + // rather than as a Render argument. Defaults to 0; shaping sources that know the cluster + // (e.g. Avalonia's GlyphInfo.GlyphCluster) set it so grapheme-aware renderers can group correctly. + public int GraphemeIndex { get; set; } + + // Required, not optional: attributes drive metric selection (subscript/superscript scaling in + // FontGlyphMetrics' ctor) and both feed the synthesized TextRun and TryGetGlyphMetrics(id, ...). + public TextAttributes TextAttributes { get; set; } = TextAttributes.None; + public TextDecorations TextDecorations { get; set; } = TextDecorations.None; + + protected internal virtual TextRun CreateTextRun() + => new() + { + Start = this.GraphemeIndex, + End = this.GraphemeIndex + 1, + Font = this.Font, + TextAttributes = this.TextAttributes, + TextDecorations = this.TextDecorations + }; +} +``` + +**The class must stay unsealed.** Downstream renderers need to derive richer option types — e.g. +ImageSharp.Drawing's `RichGlyphOptions : GlyphOptions` adding per-glyph `Pen`/`Brush` +so individual glyphs can be filled/stroked differently. The base render path reads the common members +above, then calls `CreateTextRun` and passes that run through `GlyphRendererParameters.TextRun`. +Drawing can override `CreateTextRun` to return a `RichTextRun`, which preserves the existing +`RichTextGlyphRenderer` extension point without adding Drawing-specific API to Fonts. Keep the base +type's members non-`init`-only where a caller or subclass needs to layer on per-glyph state, and avoid +`sealed`/`record` forms that would block inheritance. + +`TextAttributes`/`TextDecorations` are not cosmetic extras here: `TryGetGlyphMetrics(glyphId, +textAttributes, textDecorations, …)` takes them directly, the `FontGlyphMetrics` constructor uses +`TextAttributes` to compute the subscript/superscript scale factor and offset, and the synthesized +single-glyph `TextRun` must carry both so `RenderTo`/`RenderDecorationsTo` behave like the text path. + +### C. Public render entry — id-based + +`TextRenderer` accepts the **glyph id**, not a pre-fetched `Glyph`, and owns everything internally: +fetch metrics (with the options' attributes) → clone for rendering → synthesize a single-glyph +`TextRun` → drive the refactored render core. The caller never touches `FontGlyphMetrics`/`Glyph`, +and a stale/mismatched `Glyph` is structurally impossible. The render position comes from +`GlyphOptions.Origin` (mirroring `TextOptions.Origin`), not a separate argument. +```csharp +// Single glyph, positioned via GlyphOptions.Origin. +public static void Render( + IGlyphRenderer renderer, ushort glyphId, GlyphOptions options); +``` +`Render` uses `options.CreateTextRun()` for the render clone, so derived options can carry +per-glyph state into the renderer via a derived `TextRun`. + +**Grapheme index — must carry the real cluster.** The index is not a positional argument; it lives +on `GlyphOptions.GraphemeIndex` and flows into the synthesized `TextRun` and +`GlyphRendererParameters.GraphemeIndex`. It is **load-bearing**: `BaseGlyphBuilder.BeginGlyph` +(`ImageSharp.Drawing/Text/BaseGlyphBuilder.cs:135`), the base class of `RichTextGlyphRenderer`, +accumulates each glyph's paths into a per-grapheme `GlyphPathCollection` and flushes it whenever +`parameters.GraphemeIndex` changes. A constant `0` would merge the **entire run into a single +grapheme group**, destroying those boundaries (per-grapheme path grouping, layer/brush application). +Therefore the caller must feed the shaping cluster: the Avalonia backend sets +`GlyphOptions.GraphemeIndex = GlyphInfo.GlyphCluster` per glyph. It defaults to `0` only for callers +that genuinely render one glyph in isolation. + +**No run overload.** `GlyphOptions` is per-glyph state — the per-glyph `Pen`/`Brush` on +`RichGlyphOptions` is exactly what varies across a run — so any run-level helper taking one options +object would contradict the feature, and one taking per-glyph options (`ReadOnlySpan` +etc.) saves nothing over the caller's own loop. The caller loops `Render`, passing a fresh +(often pooled/mutated) `GlyphOptions` per glyph, and brackets the loop with its own +`BeginText`/`EndText`. +It should **not** synthesize decorations by default (run-level consumers position +underline/strikeout themselves); `GlyphOptions` can carry an opt-in. `BeginText`/`EndText` are the +caller's responsibility so a run can batch many glyphs in one text scope. + +## Colour / painted glyphs + +COLR/CPAL glyphs flow through `CffGlyphMetrics` / `PaintedGlyphMetrics` / `IPaintedGlyphSource` +(`Rendering/PaintedGlyph*.cs`) and the `BeginLayer`/`Paint`/`EndLayer` parts of `IGlyphRenderer`. +These metrics are already keyed by glyph id, so `GetGlyphMetrics(…, colorSupport: …)` returns the +painted variant for an id. The id path must pass `ColorFontSupport` through unchanged and be tested +against a COLR font so painted runs render by id, not just monochrome outlines. + +## Consumer integration (ImageSharp.Drawing Avalonia backend) + +The data is already in hand — `ImageSharpGlyphRun` (`samples/AvaloniaControlCatalog/ImageSharpGlyphRun.cs`) +exposes `GlyphTypeface`, `FontRenderingEmSize`, `BaselineOrigin`, and `GlyphInfos`, where each +`Avalonia.Media.TextFormatting.GlyphInfo` is `(ushort GlyphIndex, int GlyphCluster, double GlyphAdvance, +Vector GlyphOffset)`. + +`DrawGlyphRun` becomes: +1. Map `GlyphTypeface` → SixLabors `Font` (the typeface impl already wraps one). +2. Walk `GlyphInfos`, per glyph setting `options.Origin = BaselineOrigin + (penX, 0) + GlyphOffset` + (advancing `penX += GlyphAdvance`) and `options.GraphemeIndex = GlyphCluster` so the renderer's + per-grapheme grouping stays correct. Set `options.LayoutMode` from the run's text layout direction. +3. For each, `TextRenderer.Render(richTextGlyphRenderer, GlyphIndex, options)` — + a `RichGlyphOptions` carrying this glyph's `Origin`, `GraphemeIndex`, and the run's `Pen`/`Brush` — + into the existing `IGlyphRenderer` the backend already feeds to `DrawingCanvas`. No intermediate + `Glyph` is fetched. +4. Delete the `Text is null` gate, rendering shaped/id-only runs. + +## Risks / open questions + +- **`TextOptions` is heavy and layout-centric.** Introducing `GlyphOptions` avoids forcing + callers through the shaping/layout machinery. Confirm naming + that nothing essential is lost. +- **`GlyphRendererParameters` exposes `TextRun`/`CodePoint` publicly** and is used as a cache key. + Synthesized defaults must be deterministic so id-rendered glyphs cache correctly and don't collide + with text-rendered ones. Settled: `GraphemeIndex` carries the shaping cluster (Avalonia's + `GlyphCluster`) — it is consumed by `BaseGlyphBuilder`'s per-grapheme grouping, so a constant would + be wrong; `CodePoint` is the value recovered via `TryGetCodePoint`, falling back to `default` when + the id has no codepoint. +- **Decorations** should be opt-out by default on the id path. +- **Public API surface.** All of A and C are additive public API on a stable library — needs + maintainer review and a `PublicApiAnalyzers` shipped/unshipped update. +- **Vertical layout & substitution.** The id path renders the id as given; it performs no GSUB/GPOS + (the caller already shaped). Document that explicitly. + +## Test plan + +- `TryGetGlyphMetrics(glyphId, …)` returns metrics matching the codepoint path for known ids + (TrueType, CFF, COLR). +- Render a known id to a recording `IGlyphRenderer`; assert the emitted figure commands match the + text-driven render of the same glyph (golden path equality). +- Round-trip: `TryGetGlyphId(cp) → Render(id)` equals `Render(cp)` for simple Latin. +- COLR font: painted layers emitted for a colour glyph rendered by id. +- Vertical + rotated layout modes produce the same bounds as the text path. + +## Phasing + +1. **Fonts – accessors + refactor.** Add `TryGetGlyphMetrics(id)`; extract the + `RenderTo` core away from `TextOptions`, with `TextRun` passed explicitly. Internal, fully + covered by tests. No public render entry yet. +2. **Fonts – public render entry.** `GlyphOptions` + `TextRenderer.Render`. API review + tests. +3. **ImageSharp.Drawing – consume.** Rework the Avalonia backend `DrawGlyphRun`; verify visual parity + against the Skia backend on the text-heavy catalog pages. diff --git a/src/SixLabors.Fonts/GlyphOptions.cs b/src/SixLabors.Fonts/GlyphOptions.cs new file mode 100644 index 000000000..89bfa4921 --- /dev/null +++ b/src/SixLabors.Fonts/GlyphOptions.cs @@ -0,0 +1,116 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.Fonts.Tables.AdvancedTypographic; +using SixLabors.Fonts.Unicode; + +namespace SixLabors.Fonts; + +/// +/// Provides configuration options for rendering a glyph by id. +/// +public class GlyphOptions +{ + private float dpi = 72F; + private Font? font; + + /// + public required Font Font + { + get => this.font!; + set + { + Guard.NotNull(value, nameof(this.Font)); + this.font = value; + } + } + + /// + public float Dpi + { + get => this.dpi; + + set + { + Guard.MustBeGreaterThanOrEqualTo(value, 0, nameof(this.Dpi)); + this.dpi = value; + } + } + + /// + public HintingMode HintingMode { get; set; } + + /// + public LayoutMode LayoutMode { get; set; } = LayoutMode.HorizontalTopBottom; + + /// + public ColorFontSupport ColorFontSupport { get; set; } = ColorFontSupport.None; + + /// + public Vector2 Origin { get; set; } = Vector2.Zero; + + /// + /// Gets or sets the zero-based grapheme cluster index represented by the glyph. + /// + /// + /// This value is passed to glyph renderers through . + /// Callers rendering shaped glyph runs should set this from the shaping cluster so grapheme-aware renderers can + /// group glyph paths correctly. The default value is suitable for rendering a single isolated glyph. + /// + public int GraphemeIndex { get; set; } + + /// + /// Gets or sets the text attributes applied to the glyph. + /// + public TextAttributes TextAttributes { get; set; } = TextAttributes.None; + + /// + /// Gets or sets the text decorations applied to the glyph. + /// + public TextDecorations TextDecorations { get; set; } = TextDecorations.None; + + /// + public DecorationPositioningMode DecorationPositioningMode { get; set; } + + /// + public TextDecorationSkipInk TextDecorationSkipInk { get; set; } = TextDecorationSkipInk.Auto; + + /// + /// Creates the text run associated with the glyph. + /// + /// The text run associated with the glyph. + protected internal virtual TextRun CreateTextRun() + => new() + { + Start = this.GraphemeIndex, + End = this.GraphemeIndex + 1, + Font = this.Font, + TextAttributes = this.TextAttributes, + TextDecorations = this.TextDecorations + }; + + /// + /// Resolves the per-glyph layout mode for these options. Vertical-mixed layouts choose + /// per code point: upright for glyphs that render vertically, rotated for the rest. + /// Rendering and measurement share this mapping so their results always agree. + /// + /// The code point represented by the glyph. + /// The per-glyph layout mode. + internal GlyphLayoutMode GetGlyphLayoutMode(CodePoint codePoint) + { + if (this.LayoutMode.IsVertical()) + { + return GlyphLayoutMode.Vertical; + } + + if (this.LayoutMode.IsVerticalMixed()) + { + return AdvancedTypographicUtils.IsVerticalGlyph(codePoint, this.LayoutMode) + ? GlyphLayoutMode.Vertical + : GlyphLayoutMode.VerticalRotated; + } + + return GlyphLayoutMode.Horizontal; + } +} diff --git a/src/SixLabors.Fonts/GlyphRun.cs b/src/SixLabors.Fonts/GlyphRun.cs new file mode 100644 index 000000000..2adba8e5b --- /dev/null +++ b/src/SixLabors.Fonts/GlyphRun.cs @@ -0,0 +1,43 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.Fonts; + +/// +/// Represents positioned glyph ids that share one set of glyph rendering options. +/// +public sealed class GlyphRun +{ + /// + /// Initializes a new instance of the class. + /// + /// The glyph identifiers. + /// The glyph origins. + public GlyphRun(ReadOnlyMemory glyphIds, ReadOnlyMemory origins) + { + if (glyphIds.Length != origins.Length) + { + throw new ArgumentException("Glyph id and origin counts must match.", nameof(origins)); + } + + this.GlyphIds = glyphIds; + this.Origins = origins; + } + + /// + /// Gets the glyph identifiers. + /// + public ReadOnlyMemory GlyphIds { get; } + + /// + /// Gets the glyph origins. + /// + public ReadOnlyMemory Origins { get; } + + /// + /// Gets the number of glyphs in the run. + /// + public int Count => this.GlyphIds.Length; +} diff --git a/src/SixLabors.Fonts/IFontMetricsCollection.cs b/src/SixLabors.Fonts/IFontMetricsCollection.cs index 4225a713a..f0c926cf2 100644 --- a/src/SixLabors.Fonts/IFontMetricsCollection.cs +++ b/src/SixLabors.Fonts/IFontMetricsCollection.cs @@ -23,4 +23,19 @@ internal interface IFontMetricsCollection : IReadOnlyFontMetricsCollection /// /// The font metrics to add. public void AddMetrics(FontMetrics metrics); + + /// + /// Adds the font metrics to the under the specified family name. + /// + /// The font metrics to add. + /// The family name to use for collection lookups. + public void AddMetrics(FontMetrics metrics, string familyName); + + /// + /// Adds the font metrics to the under the specified family name and style. + /// + /// The font metrics to add. + /// The family name to use for collection lookups. + /// The style to use for collection lookups. + public void AddMetrics(FontMetrics metrics, string familyName, FontStyle style); } diff --git a/src/SixLabors.Fonts/IReadOnlySystemFontCollection.cs b/src/SixLabors.Fonts/IReadOnlySystemFontCollection.cs index c81818361..4ab2d0eae 100644 --- a/src/SixLabors.Fonts/IReadOnlySystemFontCollection.cs +++ b/src/SixLabors.Fonts/IReadOnlySystemFontCollection.cs @@ -1,6 +1,9 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Globalization; +using SixLabors.Fonts.Unicode; + namespace SixLabors.Fonts; /// @@ -14,4 +17,20 @@ public interface IReadOnlySystemFontCollection : IReadOnlyFontCollection /// /// public IEnumerable SearchDirectories { get; } + + /// + /// Tries to match a character to an installed system font. + /// + /// The code point to match. + /// The requested font style. + /// The requested font family name, or . + /// The culture used for matching, or . + /// + /// When this method returns, contains the matched font if one was found; otherwise, the default value. + /// This parameter is passed uninitialized. + /// + /// + /// if a matching system font was found; otherwise, . + /// + public bool TryMatchCharacter(CodePoint codePoint, FontStyle style, string? familyName, CultureInfo? culture, out FontMatch match); } diff --git a/src/SixLabors.Fonts/IReadonlyFontMetricsCollection.cs b/src/SixLabors.Fonts/IReadonlyFontMetricsCollection.cs index 4816825a1..1423c80e2 100644 --- a/src/SixLabors.Fonts/IReadonlyFontMetricsCollection.cs +++ b/src/SixLabors.Fonts/IReadonlyFontMetricsCollection.cs @@ -30,6 +30,22 @@ internal interface IReadOnlyFontMetricsCollection /// is public bool TryGetMetrics(string name, CultureInfo culture, FontStyle style, [NotNullWhen(true)] out FontMetrics? metrics); + /// + /// Gets the specified font metrics matching the given culture, font family name, style, and numeric weight. + /// + /// The font family name. + /// The culture to use when searching for a match. + /// The font style to use when searching for a match. + /// The numeric font weight to use when searching for a match. + /// The matching font metrics, if found. + /// if the collection contains a matching face; otherwise, . + public bool TryGetMetrics( + string name, + CultureInfo culture, + FontStyle style, + FontWeight weight, + [NotNullWhen(true)] out FontMetrics? metrics); + /// /// Gets the collection of available font metrics for a given culture and font family name. /// diff --git a/src/SixLabors.Fonts/KnownVariationAxes.cs b/src/SixLabors.Fonts/KnownVariationAxes.cs index 0a724b51d..4db3f82c2 100644 --- a/src/SixLabors.Fonts/KnownVariationAxes.cs +++ b/src/SixLabors.Fonts/KnownVariationAxes.cs @@ -3,6 +3,8 @@ namespace SixLabors.Fonts; +using SixLabors.Fonts.Tables.AdvancedTypographic; + /// /// Defines the registered design-variation axis tags for variable fonts. /// These tags are used with to control font design axes. @@ -15,28 +17,28 @@ public static class KnownVariationAxes /// Value range: 0 (upright) to 1 (italic). /// /// - public const string Italic = "ital"; + public static readonly Tag Italic = Tag.Parse("ital"); /// /// Optical size axis ('opsz'). Adjusts the design for a specific text size in points. /// Typical range: 6 to 144. Larger values optimize for display use; smaller for body text. /// /// - public const string OpticalSize = "opsz"; + public static readonly Tag OpticalSize = Tag.Parse("opsz"); /// /// Slant axis ('slnt'). Controls the slant angle of upright glyphs in degrees. /// Typical range: -90 to 90. Negative values slant to the right (the common direction). /// /// - public const string Slant = "slnt"; + public static readonly Tag Slant = Tag.Parse("slnt"); /// /// Width axis ('wdth'). Controls the relative width of the font as a percentage of normal. /// Typical range: 75 (condensed) to 125 (expanded). 100 represents the normal width. /// /// - public const string Width = "wdth"; + public static readonly Tag Width = Tag.Parse("wdth"); /// /// Weight axis ('wght'). Controls the weight (boldness) of the font. @@ -44,5 +46,5 @@ public static class KnownVariationAxes /// 700 (Bold), 900 (Black). /// /// - public const string Weight = "wght"; + public static readonly Tag Weight = Tag.Parse("wght"); } diff --git a/src/SixLabors.Fonts/LineLayout.cs b/src/SixLabors.Fonts/LineLayout.cs index dd949f0f3..ee8a4593d 100644 --- a/src/SixLabors.Fonts/LineLayout.cs +++ b/src/SixLabors.Fonts/LineLayout.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. using System.Numerics; @@ -44,6 +44,12 @@ internal LineLayout( /// public LineMetrics LineMetrics { get; } + /// + /// Gets a value indicating whether underline and overline decorations skip over glyph ink + /// when this line is rendered. + /// + public TextDecorationSkipInk TextDecorationSkipInk => this.options.TextDecorationSkipInk; + /// /// Gets the grapheme metrics entries for this line in final layout order. /// diff --git a/src/SixLabors.Fonts/MemoryFontMetrics.cs b/src/SixLabors.Fonts/MemoryFontMetrics.cs new file mode 100644 index 000000000..1e89ab498 --- /dev/null +++ b/src/SixLabors.Fonts/MemoryFontMetrics.cs @@ -0,0 +1,259 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics.CodeAnalysis; +using System.Numerics; +using SixLabors.Fonts.Tables; +using SixLabors.Fonts.Tables.AdvancedTypographic; +using SixLabors.Fonts.Tables.AdvancedTypographic.Variations; +using SixLabors.Fonts.Unicode; + +namespace SixLabors.Fonts; + +/// +/// +/// Represents a font face with metrics, which is a set of glyphs with a specific style (regular, italic, bold etc). +/// +/// The font source is an owned memory buffer. +/// +internal sealed class MemoryFontMetrics : FontMetrics +{ + private readonly Lazy fontMetrics; + private readonly FontSource source; + + /// + /// Initializes a new instance of the class. + /// + /// The source stream. + public MemoryFontMetrics(Stream stream) + : this(CopyToArray(stream), 0) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The source data. + /// The offset of the font within . + private MemoryFontMetrics(byte[] data, long offset) + : this(LoadDescription(data, offset), data, offset) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The font description. + /// The source data. + /// The offset of the font within . + private MemoryFontMetrics(FontDescription description, byte[] data, long offset) + { + this.Description = description; + this.source = FontSource.Create(data, offset); + this.fontMetrics = new Lazy(this.LoadFont, true); + } + + /// + public override FontDescription Description { get; } + + /// + /// Gets the underlying that this memory-backed instance delegates to. + /// + public StreamFontMetrics StreamFontMetrics => this.fontMetrics.Value; + + /// + public override ushort UnitsPerEm => this.fontMetrics.Value.UnitsPerEm; + + /// + public override float ScaleFactor => this.fontMetrics.Value.ScaleFactor; + + /// + public override HorizontalMetrics HorizontalMetrics => this.fontMetrics.Value.HorizontalMetrics; + + /// + public override VerticalMetrics VerticalMetrics => this.fontMetrics.Value.VerticalMetrics; + + /// + public override short SubscriptXSize => this.fontMetrics.Value.SubscriptXSize; + + /// + public override short SubscriptYSize => this.fontMetrics.Value.SubscriptYSize; + + /// + public override short SubscriptXOffset => this.fontMetrics.Value.SubscriptXOffset; + + /// + public override short SubscriptYOffset => this.fontMetrics.Value.SubscriptYOffset; + + /// + public override short SuperscriptXSize => this.fontMetrics.Value.SuperscriptXSize; + + /// + public override short SuperscriptYSize => this.fontMetrics.Value.SuperscriptYSize; + + /// + public override short SuperscriptXOffset => this.fontMetrics.Value.SuperscriptXOffset; + + /// + public override short SuperscriptYOffset => this.fontMetrics.Value.SuperscriptYOffset; + + /// + public override short StrikeoutSize => this.fontMetrics.Value.StrikeoutSize; + + /// + public override short StrikeoutPosition => this.fontMetrics.Value.StrikeoutPosition; + + /// + public override short UnderlinePosition => this.fontMetrics.Value.UnderlinePosition; + + /// + public override short UnderlineThickness => this.fontMetrics.Value.UnderlineThickness; + + /// + public override float ItalicAngle => this.fontMetrics.Value.ItalicAngle; + + /// + public override bool TryGetTableData(Tag tag, out ReadOnlyMemory table) + => this.source.TryGetTableData(tag, out table); + + /// + public override Stream OpenStream() + => this.source.OpenStream(); + + /// + internal override bool TryGetGlyphId(CodePoint codePoint, out ushort glyphId) + => this.fontMetrics.Value.TryGetGlyphId(codePoint, out glyphId); + + /// + internal override bool TryGetGlyphId( + CodePoint codePoint, + CodePoint? nextCodePoint, + out ushort glyphId, + out bool skipNextCodePoint) + => this.fontMetrics.Value.TryGetGlyphId(codePoint, nextCodePoint, out glyphId, out skipNextCodePoint); + + /// + internal override bool TryGetCodePoint(ushort glyphId, out CodePoint codePoint) + => this.fontMetrics.Value.TryGetCodePoint(glyphId, out codePoint); + + /// + internal override bool TryGetGlyphClass(ushort glyphId, [NotNullWhen(true)] out GlyphClassDef? glyphClass) + => this.fontMetrics.Value.TryGetGlyphClass(glyphId, out glyphClass); + + /// + internal override bool TryGetMarkAttachmentClass(ushort glyphId, [NotNullWhen(true)] out GlyphClassDef? markAttachmentClass) + => this.fontMetrics.Value.TryGetMarkAttachmentClass(glyphId, out markAttachmentClass); + + /// + public override bool TryGetVariationAxes(out ReadOnlyMemory variationAxes) + => this.fontMetrics.Value.TryGetVariationAxes(out variationAxes); + + /// + internal override bool IsInMarkFilteringSet(ushort markGlyphSetIndex, ushort glyphId) + => this.fontMetrics.Value.IsInMarkFilteringSet(markGlyphSetIndex, glyphId); + + /// + public override bool TryGetGlyphMetrics( + CodePoint codePoint, + TextAttributes textAttributes, + TextDecorations textDecorations, + LayoutMode layoutMode, + ColorFontSupport support, + [NotNullWhen(true)] out FontGlyphMetrics? metrics) + => this.fontMetrics.Value.TryGetGlyphMetrics(codePoint, textAttributes, textDecorations, layoutMode, support, out metrics); + + /// + public override bool TryGetGlyphMetrics( + ushort glyphId, + TextAttributes textAttributes, + TextDecorations textDecorations, + LayoutMode layoutMode, + ColorFontSupport support, + [NotNullWhen(true)] out FontGlyphMetrics? metrics) + => this.fontMetrics.Value.TryGetGlyphMetrics(glyphId, textAttributes, textDecorations, layoutMode, support, out metrics); + + /// + internal override FontGlyphMetrics GetGlyphMetrics( + CodePoint codePoint, + ushort glyphId, + TextAttributes textAttributes, + TextDecorations textDecorations, + LayoutMode layoutMode, + ColorFontSupport support) + => this.fontMetrics.Value.GetGlyphMetrics(codePoint, glyphId, textAttributes, textDecorations, layoutMode, support); + + /// + public override ReadOnlyMemory GetAvailableCodePoints() + => this.fontMetrics.Value.GetAvailableCodePoints(); + + /// + internal override bool TryGetGSubTable([NotNullWhen(true)] out GSubTable? gSubTable) + => this.fontMetrics.Value.TryGetGSubTable(out gSubTable); + + /// + internal override void ApplySubstitution(GlyphSubstitutionCollection collection) + => this.fontMetrics.Value.ApplySubstitution(collection); + + /// + internal override bool TryGetKerningOffset(ushort currentId, ushort nextId, out Vector2 vector) + => this.fontMetrics.Value.TryGetKerningOffset(currentId, nextId, out vector); + + /// + internal override void UpdatePositions(GlyphPositioningCollection collection) + => this.fontMetrics.Value.UpdatePositions(collection); + + /// + internal override float GetGDefVariationDelta(uint packedVariationIndex) + => this.fontMetrics.Value.GetGDefVariationDelta(packedVariationIndex); + + /// + internal override ReadOnlySpan GetNormalizedCoordinates() + => this.fontMetrics.Value.GetNormalizedCoordinates(); + + /// + /// Reads a collection of instances from the specified stream. + /// + /// The source stream. + /// A read-only memory region containing the font metrics. + public static ReadOnlyMemory LoadFontCollection(Stream stream) + { + byte[] data = CopyToArray(stream); + using MemoryStream memory = new(data, writable: false); + using BigEndianBinaryReader reader = new(memory, true); + TtcHeader ttcHeader = TtcHeader.Read(reader); + MemoryFontMetrics[] fonts = new MemoryFontMetrics[(int)ttcHeader.NumFonts]; + + for (int i = 0; i < ttcHeader.NumFonts; ++i) + { + long offset = ttcHeader.OffsetTable[i]; + using MemoryStream faceStream = new(data, writable: false); + faceStream.Position = offset; + using FontReader fontReader = new(faceStream); + FontDescription description = FontDescription.LoadDescription(fontReader); + fonts[i] = new MemoryFontMetrics(description, data, offset); + } + + return fonts; + } + + private static byte[] CopyToArray(Stream stream) + { + using MemoryStream memory = new(); + stream.CopyTo(memory); + return memory.ToArray(); + } + + private static FontDescription LoadDescription(byte[] data, long offset) + { + using MemoryStream stream = new(data, writable: false); + stream.Position = offset; + using FontReader reader = new(stream); + return FontDescription.LoadDescription(reader); + } + + private StreamFontMetrics LoadFont() + { + using Stream stream = this.OpenStream(); + return StreamFontMetrics.LoadFont(stream, this.source); + } +} diff --git a/src/SixLabors.Fonts/Native/CFStringEncoding.cs b/src/SixLabors.Fonts/Native/CFStringEncoding.cs deleted file mode 100644 index 2d10edfc1..000000000 --- a/src/SixLabors.Fonts/Native/CFStringEncoding.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Diagnostics.CodeAnalysis; - -namespace SixLabors.Fonts.Native; - -/// -/// An integer type for constants used to specify supported string encodings in various CFString functions. -/// -[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1300:Element should begin with upper-case letter", Justification = "Verbatim constants from the macOS SDK")] -internal enum CFStringEncoding : uint -{ - /// - /// An encoding constant that identifies the UTF 8 encoding. - /// - kCFStringEncodingUTF8 = 0x08000100, - - /// - /// An encoding constant that identifies kTextEncodingUnicodeDefault + kUnicodeUTF16LEFormat encoding. This constant specifies little-endian byte order. - /// - kCFStringEncodingUTF16LE = 0x14000100, -} diff --git a/src/SixLabors.Fonts/Native/CoreFoundation.cs b/src/SixLabors.Fonts/Native/CoreFoundation.cs index 2bb3aabef..437b66b50 100644 --- a/src/SixLabors.Fonts/Native/CoreFoundation.cs +++ b/src/SixLabors.Fonts/Native/CoreFoundation.cs @@ -6,27 +6,36 @@ namespace SixLabors.Fonts.Native; // ReSharper disable InconsistentNaming -internal static class CoreFoundation +internal static partial class CoreFoundation { private const string CoreFoundationFramework = "/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation"; /// - /// Returns the number of values currently in an array. + /// Defines Core Foundation number storage types used by native font metadata. + /// The native CFNumberType is a CFIndex, which is 8 bytes on 64-bit + /// platforms, so the enum must be backed by to match the ABI. /// - /// The array to examine. - /// The number of values in . - [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 long CFArrayGetCount(IntPtr theArray); + internal enum CFNumberType : long + { + /// + /// A signed 32-bit integer value. + /// + SInt32 = 3, + + /// + /// A Core Graphics floating-point value. + /// + CGFloat = 16 + } /// - /// Returns the type identifier for the CFArray opaque type. + /// Returns the number of values currently in an array. /// - /// The type identifier for the CFArray opaque type. - /// CFMutableArray objects have the same type identifier as CFArray objects. - [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 ulong CFArrayGetTypeID(); + /// The array to examine. + /// The number of values in . + [LibraryImport(CoreFoundationFramework)] + [UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })] + public static partial long CFArrayGetCount(IntPtr theArray); /// /// Retrieves a value at a given index. @@ -34,9 +43,9 @@ internal static class CoreFoundation /// The array to examine. /// The index of the value to retrieve. If the index is outside the index space of (0 to N-1 inclusive where N is the count of ), the behavior is undefined. /// The value at the index in . If the return value is a Core Foundation Object, ownership follows The Get Rule. - [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 IntPtr CFArrayGetValueAtIndex(IntPtr theArray, long idx); + [LibraryImport(CoreFoundationFramework)] + [UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })] + public static partial IntPtr CFArrayGetValueAtIndex(IntPtr theArray, long idx); /// /// Returns the unique identifier of an opaque type to which a Core Foundation object belongs. @@ -48,60 +57,109 @@ internal static class CoreFoundation /// 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)] - [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 ulong CFGetTypeID(IntPtr cf); + [LibraryImport(CoreFoundationFramework)] + [UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })] + public static partial ulong CFGetTypeID(IntPtr cf); + + /// + /// Returns the type identifier for the CFDictionary opaque type. + /// + /// The type identifier for the CFDictionary opaque type. + [LibraryImport(CoreFoundationFramework)] + [UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })] + public static partial ulong CFDictionaryGetTypeID(); + + /// + /// Retrieves a dictionary value for the specified key without retaining the value. + /// + /// The dictionary to inspect. + /// The key to find. + /// The matching value. + /// when the key exists; otherwise, . + [LibraryImport(CoreFoundationFramework)] + [UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })] + [return: MarshalAs(UnmanagedType.I1)] + public static partial bool CFDictionaryGetValueIfPresent(IntPtr theDict, IntPtr key, out IntPtr value); + + /// + /// Returns the type identifier for the CFNumber opaque type. + /// + /// The type identifier for the CFNumber opaque type. + [LibraryImport(CoreFoundationFramework)] + [UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })] + public static partial ulong CFNumberGetTypeID(); + + /// + /// Copies a Core Foundation number into a double precision floating-point value. + /// + /// The number to read. + /// The requested output type. + /// The converted value. + /// when conversion succeeds; otherwise, . + [LibraryImport(CoreFoundationFramework, EntryPoint = "CFNumberGetValue")] + [UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })] + [return: MarshalAs(UnmanagedType.I1)] + public static partial bool CFNumberGetDoubleValue(IntPtr number, CFNumberType theType, out double value); + + /// + /// Copies a Core Foundation number into a signed 32-bit integer value. + /// + /// The number to read. + /// The requested output type. + /// The converted value. + /// when conversion succeeds; otherwise, . + [LibraryImport(CoreFoundationFramework, EntryPoint = "CFNumberGetValue")] + [UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })] + [return: MarshalAs(UnmanagedType.I1)] + public static partial bool CFNumberGetIntValue(IntPtr number, CFNumberType theType, out int value); /// /// Returns the number (in terms of UTF-16 code pairs) of Unicode characters in a string. /// /// The string to examine. /// The number (in terms of UTF-16 code pairs) of characters stored in . - [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 long CFStringGetLength(IntPtr theString); + [LibraryImport(CoreFoundationFramework)] + [UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })] + public static partial long CFStringGetLength(IntPtr theString); /// - /// Copies the character contents of a string to a local C string buffer after converting the characters to a given encoding. + /// Copies the UTF-16 characters of a string into a caller-provided buffer. /// /// The string whose contents you wish to access. - /// - /// The C string buffer into which to copy the string. On return, the buffer contains the converted characters. If there is an error in conversion, the buffer contains only partial results. - /// The buffer must be large enough to contain the converted characters and a NUL terminator. For example, if the string is Toby, the buffer must be at least 5 bytes long. - /// - /// 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. - [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); + /// The range of characters to copy. + /// The caller-provided UTF-16 character buffer. + [LibraryImport(CoreFoundationFramework)] + [UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })] + public static unsafe partial void CFStringGetCharacters(IntPtr theString, CoreText.CFRange range, char* buffer); /// - /// Quickly obtains a pointer to a C-string buffer containing the characters of a string in a given encoding. + /// Creates an immutable string from UTF-16 characters. /// - /// The string whose contents you wish to access. - /// The string encoding to which the character contents of should be converted. The encoding must specify an 8-bit encoding. - /// A pointer to a C string or NULL if the internal storage of does not allow this to be returned efficiently. + /// The allocator to use, or NULL for the default allocator. + /// The UTF-16 characters. + /// The number of characters to read from . + /// + /// A retained CFString, or NULL on error. The caller is responsible for releasing + /// the returned string. + /// /// - /// - /// This function either returns the requested pointer immediately, with no memory allocations and no copying, in constant time, or returns NULL. If the latter is the result, call an alternative function such as the function to extract the characters. - /// - /// - /// Whether or not this function returns a valid pointer or NULL depends on many factors, all of which depend on how the string was created and its properties. In addition, the function result might change between different releases and on different platforms. So do not count on receiving a non-NULL result from this function under any circumstances. - /// + /// Core Foundation follows the Create Rule for this API, so a non-NULL result must be + /// released with . /// - [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 IntPtr CFStringGetCStringPtr(IntPtr theString, CFStringEncoding encoding); + [LibraryImport(CoreFoundationFramework)] + [UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })] + public static unsafe partial IntPtr CFStringCreateWithCharacters( + IntPtr allocator, + char* characters, + nint numberOfCharacters); /// /// Releases a Core Foundation object. /// /// A CFType object to release. This value must not be NULL. - [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 void CFRelease(IntPtr cf); + [LibraryImport(CoreFoundationFramework)] + [UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })] + public static partial void CFRelease(IntPtr cf); /// /// Returns the path portion of a given URL. @@ -110,15 +168,15 @@ internal static class CoreFoundation /// The operating system path style to be used to create the path. See for a list of possible values. /// The URL's path in the format specified by . Ownership follows the create rule. See The Create Rule. /// This function returns the URL's path as a file system path for a given path style. - [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 IntPtr CFURLCopyFileSystemPath(IntPtr anURL, CFURLPathStyle pathStyle); + [LibraryImport(CoreFoundationFramework)] + [UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })] + public static partial IntPtr CFURLCopyFileSystemPath(IntPtr anURL, CFURLPathStyle pathStyle); /// /// Returns the type identifier for the CFURL opaque type. /// /// The type identifier for the CFURL opaque type. - [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 ulong CFURLGetTypeID(); + [LibraryImport(CoreFoundationFramework)] + [UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })] + public static partial ulong CFURLGetTypeID(); } diff --git a/src/SixLabors.Fonts/Native/CoreText.cs b/src/SixLabors.Fonts/Native/CoreText.cs index ebdb1737d..6a59864cf 100644 --- a/src/SixLabors.Fonts/Native/CoreText.cs +++ b/src/SixLabors.Fonts/Native/CoreText.cs @@ -5,15 +5,296 @@ namespace SixLabors.Fonts.Native; -internal static class CoreText +// ReSharper disable InconsistentNaming + +/// +/// Provides native CoreText entry points used by the macOS system font implementation. +/// +/// +/// These declarations bind to the system CoreText framework at +/// /System/Library/Frameworks/CoreText.framework/Versions/A/CoreText and must only be +/// called from macOS-specific paths. +/// +internal static partial class CoreText { private const string CoreTextFramework = "/System/Library/Frameworks/CoreText.framework/Versions/A/CoreText"; + private static readonly Lazy LazyCoreTextLibrary = new(() => NativeLibrary.Load(CoreTextFramework), isThreadSafe: true); + + /// + /// Defines the CoreText user-interface font family to create. + /// + internal enum CTFontUIFontType + { + /// + /// The default system user-interface font. + /// + System = 2 + } + + /// + /// Returns an array of font family names. + /// + /// + /// A retained CFArray of CFStringRef objects representing the available font + /// family names, or NULL on error. The caller is responsible for releasing the returned array. + /// + /// + /// CoreText follows Core Foundation's Copy Rule for this API, so a non-NULL result + /// must be released with . + /// + [LibraryImport(CoreTextFramework)] + [UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })] + public static partial IntPtr CTFontManagerCopyAvailableFontFamilyNames(); + + /// + /// Creates a collection containing the available fonts. + /// + /// Collection options, or NULL. + /// + /// A retained CTFontCollection, or NULL on error. The caller is responsible for + /// releasing the returned collection. + /// + /// + /// CoreText follows the Create Rule for this API, so a non-NULL result must be + /// released with . + /// + [LibraryImport(CoreTextFramework)] + [UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })] + public static partial IntPtr CTFontCollectionCreateFromAvailableFonts(IntPtr options); + + /// + /// Creates descriptors for fonts matching a collection. + /// + /// The font collection. + /// + /// A retained CFArray of CTFontDescriptor objects, or NULL on error. + /// The caller is responsible for releasing the returned array. + /// + /// + /// CoreText follows the Create Rule for this API, so a non-NULL result must be + /// released with . + /// + [LibraryImport(CoreTextFramework)] + [UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })] + public static partial IntPtr CTFontCollectionCreateMatchingFontDescriptors(IntPtr collection); + + /// + /// Copies an attribute value from a font descriptor. + /// + /// The font descriptor. + /// The descriptor attribute key. + /// + /// A retained Core Foundation object, or NULL when the attribute is not present. + /// The caller is responsible for releasing the returned object. + /// + /// + /// CoreText follows the Copy Rule for this API, so a non-NULL result must be + /// released with . + /// + [LibraryImport(CoreTextFramework)] + [UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })] + public static partial IntPtr CTFontDescriptorCopyAttribute(IntPtr descriptor, IntPtr attribute); + + /// + /// Creates a font reference from a font descriptor. + /// + /// The font descriptor. + /// The font size. + /// The font matrix, or NULL. + /// + /// A retained CTFont, or NULL on error. The caller is responsible for releasing + /// the returned font. + /// + /// + /// CoreText follows the Create Rule for this API, so a non-NULL result must be + /// released with . + /// + [LibraryImport(CoreTextFramework)] + [UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })] + public static partial IntPtr CTFontCreateWithFontDescriptor(IntPtr descriptor, double size, IntPtr matrix); + + /// + /// Creates a font reference from a font or family name. + /// + /// The font or family name. + /// The font size. + /// The font matrix, or NULL. + /// + /// A retained CTFont, or NULL on error. The caller is responsible for releasing + /// the returned font. + /// + /// + /// CoreText follows the Create Rule for this API, so a non-NULL result must be + /// released with . + /// + [LibraryImport(CoreTextFramework)] + [UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })] + public static partial IntPtr CTFontCreateWithName(IntPtr name, double size, IntPtr matrix); + + /// + /// Creates a copy of a font with symbolic traits applied. + /// + /// The font to copy. + /// The font size. + /// The font matrix, or NULL. + /// The symbolic trait values to set. + /// The symbolic traits to replace. + /// + /// A retained CTFont, or NULL on error. The caller is responsible for releasing + /// the returned font. + /// + /// + /// CoreText follows the Create Rule for this API, so a non-NULL result must be + /// released with . + /// + [LibraryImport(CoreTextFramework)] + [UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })] + public static partial IntPtr CTFontCreateCopyWithSymbolicTraits( + IntPtr font, + double size, + IntPtr matrix, + uint symbolicTraitValue, + uint symbolicTraitMask); /// - /// Returns an array of font URLs. + /// Creates a user-interface font reference. /// - /// This function returns a retained reference to a CFArray of CFURLRef objects representing the URLs of the available fonts, or NULL on error. The caller is responsible for releasing the array. - [DllImport(CoreTextFramework, 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 IntPtr CTFontManagerCopyAvailableFontURLs(); + /// The user-interface font type. + /// The font size. + /// The requested language, or NULL. + /// + /// A retained CTFont, or NULL on error. The caller is responsible for releasing + /// the returned font. + /// + /// + /// CoreText follows the Create Rule for this API, so a non-NULL result must be + /// released with . + /// + [LibraryImport(CoreTextFramework)] + [UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })] + public static partial IntPtr CTFontCreateUIFontForLanguage(CTFontUIFontType uiType, double size, IntPtr language); + + /// + /// Creates the best matching font for a string. + /// + /// The current font used as the matching context. + /// The string to match. + /// The string range to match. + /// The requested language, or NULL. + /// + /// A retained CTFont, or NULL on error. The caller is responsible for releasing + /// the returned font. + /// + /// + /// CoreText follows the Create Rule for this API, so a non-NULL result must be + /// released with . + /// + [LibraryImport(CoreTextFramework)] + [UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })] + public static partial IntPtr CTFontCreateForStringWithLanguage( + IntPtr currentFont, + IntPtr stringRef, + CFRange range, + IntPtr language); + + /// + /// Copies a font's family name. + /// + /// The font. + /// + /// A retained CFString, or NULL on error. The caller is responsible for releasing + /// the returned string. + /// + /// + /// CoreText follows the Copy Rule for this API, so a non-NULL result must be released + /// with . + /// + [LibraryImport(CoreTextFramework)] + [UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })] + public static partial IntPtr CTFontCopyFamilyName(IntPtr font); + + /// + /// Copies a font's PostScript name. + /// + /// The font. + /// + /// A retained CFString, or NULL on error. The caller is responsible for releasing + /// the returned string. + /// + /// + /// CoreText follows the Copy Rule for this API, so a non-NULL result must be released + /// with . + /// + [LibraryImport(CoreTextFramework)] + [UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })] + public static partial IntPtr CTFontCopyPostScriptName(IntPtr font); + + /// + /// Gets the symbolic traits for a font. + /// + /// The font. + /// The symbolic traits. + [LibraryImport(CoreTextFramework)] + [UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })] + public static partial uint CTFontGetSymbolicTraits(IntPtr font); + + /// + /// Gets glyph identifiers for UTF-16 characters in a font. + /// + /// The font. + /// The UTF-16 characters. + /// The glyph output buffer. + /// The number of UTF-16 characters. + /// if every character has a glyph; otherwise, . + [LibraryImport(CoreTextFramework)] + [UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })] + [return: MarshalAs(UnmanagedType.I1)] + public static unsafe partial bool CTFontGetGlyphsForCharacters( + IntPtr font, + ushort* characters, + ushort* glyphs, + nint count); + + /// + /// Gets a CoreText exported CFStringRef constant. + /// + /// The exported symbol name. + /// The constant value, or when the symbol is unavailable. + public static IntPtr GetStringConstant(string name) + { + if (!NativeLibrary.TryGetExport(LazyCoreTextLibrary.Value, name, out IntPtr symbol)) + { + return IntPtr.Zero; + } + + return Marshal.ReadIntPtr(symbol); + } + + /// + /// Represents a Core Foundation range. + /// + [StructLayout(LayoutKind.Sequential)] + internal readonly struct CFRange + { + /// + /// Initializes a new instance of the struct. + /// + /// The starting location. + /// The range length. + public CFRange(nint location, nint length) + { + this.Location = location; + this.Length = length; + } + + /// + /// Gets the starting location. + /// + public nint Location { get; } + + /// + /// Gets the range length. + /// + public nint Length { get; } + } } diff --git a/src/SixLabors.Fonts/Native/CoreTextSystemFontMatcher.cs b/src/SixLabors.Fonts/Native/CoreTextSystemFontMatcher.cs new file mode 100644 index 000000000..4a34a4824 --- /dev/null +++ b/src/SixLabors.Fonts/Native/CoreTextSystemFontMatcher.cs @@ -0,0 +1,972 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Runtime.Versioning; +using SixLabors.Fonts.Unicode; +using SixLabors.Fonts.WellKnownIds; +using static SixLabors.Fonts.Native.CoreFoundation; +using static SixLabors.Fonts.Native.CoreText; + +namespace SixLabors.Fonts.Native; + +/// +/// Provides macOS system font fallback matching through CoreText. +/// +/// +/// This follows the same operating-system fallback shape as Skia's CoreText font manager: +/// create a base font, ask CoreText for the font that can render the requested string, then +/// use the returned font's family and traits to map back into the managed system font collection. +/// +internal static class CoreTextSystemFontMatcher +{ + private const uint CTFontItalicTrait = 1U << 0; + private const uint CTFontBoldTrait = 1U << 1; + private const double CTFontRegularWeight = 0D; + private const double CTFontBoldWeight = .4D; + private const double CTFontStyleScoreScale = 1000D; + private static readonly object FamilyFacesLock = new(); + private static NativeSystemFontFace[]? cachedFamilyFaces; + private static Dictionary? cachedFamilyNames; + + /// + /// Tries to get the macOS default font family name. + /// + /// The macOS default font family name. + /// if CoreText returned a default font family name; otherwise, . + public static bool TryGetDefaultFamilyName([NotNullWhen(true)] out string? familyName) + { + familyName = null; + + if (!OperatingSystem.IsMacOS()) + { + return false; + } + + try + { + return TryGetDefaultFamilyNameMacOS(out familyName); + } + catch (DllNotFoundException) + { + return false; + } + catch (EntryPointNotFoundException) + { + return false; + } + } + + /// + /// Tries to enumerate installed font family names through CoreText. + /// + /// The installed font family names. + /// if CoreText returned family names; otherwise, . + public static bool TryGetFamilyNames([NotNullWhen(true)] out string[]? familyNames) + { + familyNames = null; + + if (!OperatingSystem.IsMacOS()) + { + return false; + } + + try + { + return TryGetFamilyNamesMacOS(out familyNames); + } + catch (DllNotFoundException) + { + return false; + } + catch (EntryPointNotFoundException) + { + return false; + } + } + + /// + /// Tries to enumerate installed system font faces through CoreText. + /// + /// The installed system font faces. + /// if CoreText returned font faces; otherwise, . + public static bool TryGetFamilyFaces([NotNullWhen(true)] out NativeSystemFontFace[]? faces) + { + faces = null; + + if (!OperatingSystem.IsMacOS()) + { + return false; + } + + try + { + faces = GetFamilyFacesMacOS(); + return faces.Length > 0; + } + catch (DllNotFoundException) + { + return false; + } + catch (EntryPointNotFoundException) + { + return false; + } + } + + /// + /// Tries to match a character through CoreText. + /// + /// The code point to match. + /// The requested font style. + /// The requested family name. + /// The requested culture. + /// The matched family name. + /// The matched style. + /// if CoreText matched a system font; otherwise, . + public static bool TryMatchCharacter( + CodePoint codePoint, + FontStyle style, + string? familyName, + CultureInfo? culture, + out string? matchedFamilyName, + out FontStyle matchedStyle) + { + matchedFamilyName = null; + matchedStyle = default; + + if (!OperatingSystem.IsMacOS()) + { + return false; + } + + try + { + return TryMatchCharacterMacOS(codePoint, style, familyName, culture, out matchedFamilyName, out matchedStyle); + } + catch (DllNotFoundException) + { + return false; + } + catch (EntryPointNotFoundException) + { + return false; + } + } + + /// + /// Tries to enumerate installed font family names through CoreText on macOS. + /// + /// The installed font family names. + /// if CoreText returned family names; otherwise, . + [SupportedOSPlatform("macos")] + private static bool TryGetFamilyNamesMacOS([NotNullWhen(true)] out string[]? familyNames) + { + familyNames = null; + IntPtr familyNamesArray = CTFontManagerCopyAvailableFontFamilyNames(); + + try + { + if (familyNamesArray == IntPtr.Zero) + { + return false; + } + + int count = checked((int)CFArrayGetCount(familyNamesArray)); + string[] names = new string[count]; + int nameCount = 0; + + for (int i = 0; i < count; i++) + { + IntPtr familyNameString = CFArrayGetValueAtIndex(familyNamesArray, i); + + if (familyNameString != IntPtr.Zero + && TryGetString(familyNameString, out string? familyName) + && familyName is { Length: > 0 }) + { + names[nameCount++] = familyName; + } + } + + if (nameCount == 0) + { + return false; + } + + if (nameCount != names.Length) + { + Array.Resize(ref names, nameCount); + } + + familyNames = names; + return true; + } + finally + { + ReleaseIfNeeded(familyNamesArray); + } + } + + /// + /// Tries to enumerate installed system font faces through CoreText on macOS. + /// + /// The installed system font faces. + /// if CoreText returned font faces; otherwise, . + [SupportedOSPlatform("macos")] + private static bool TryLoadFamilyFacesMacOS([NotNullWhen(true)] out NativeSystemFontFace[]? faces) + { + faces = null; + IntPtr fontNameAttribute = GetStringConstant("kCTFontNameAttribute"); + IntPtr familyNameAttribute = GetStringConstant("kCTFontFamilyNameAttribute"); + IntPtr urlAttribute = GetStringConstant("kCTFontURLAttribute"); + IntPtr traitsAttribute = GetStringConstant("kCTFontTraitsAttribute"); + IntPtr weightTrait = GetStringConstant("kCTFontWeightTrait"); + IntPtr symbolicTrait = GetStringConstant("kCTFontSymbolicTrait"); + + if (fontNameAttribute == IntPtr.Zero + || familyNameAttribute == IntPtr.Zero + || urlAttribute == IntPtr.Zero + || traitsAttribute == IntPtr.Zero + || weightTrait == IntPtr.Zero + || symbolicTrait == IntPtr.Zero) + { + return false; + } + + IntPtr collection = CTFontCollectionCreateFromAvailableFonts(IntPtr.Zero); + IntPtr descriptors = IntPtr.Zero; + + try + { + if (collection == IntPtr.Zero) + { + return false; + } + + descriptors = CTFontCollectionCreateMatchingFontDescriptors(collection); + if (descriptors == IntPtr.Zero) + { + return false; + } + + int count = checked((int)CFArrayGetCount(descriptors)); + List results = new(count); + Dictionary> faceIndexesByPath = new(StringComparer.Ordinal); + + for (int i = 0; i < count; i++) + { + IntPtr descriptor = CFArrayGetValueAtIndex(descriptors, i); + + if (descriptor == IntPtr.Zero) + { + continue; + } + + IntPtr url = CTFontDescriptorCopyAttribute(descriptor, urlAttribute); + + try + { + if (url == IntPtr.Zero + || CFGetTypeID(url) != CFURLGetTypeID() + || !TryGetUrlPath(url, out string? path) + || !TryGetDescriptorStringAttribute(descriptor, familyNameAttribute, out string? familyName) + || !TryGetDescriptorStringAttribute(descriptor, fontNameAttribute, out string? fontName) + || !TryGetFaceIndex(path, fontName, faceIndexesByPath, out int faceIndex)) + { + continue; + } + + GetDescriptorTraits(descriptor, traitsAttribute, weightTrait, symbolicTrait, out double weight, out uint? symbolicTraits); + + // The traits dictionary exposes the symbolic traits without instantiating + // a font; instantiate one only for descriptors that do not carry them. + if (symbolicTraits is null) + { + IntPtr font = CTFontCreateWithFontDescriptor(descriptor, size: 0, matrix: IntPtr.Zero); + + try + { + if (font != IntPtr.Zero) + { + symbolicTraits = CTFontGetSymbolicTraits(font); + } + } + finally + { + ReleaseIfNeeded(font); + } + } + + if (symbolicTraits is null) + { + continue; + } + + FontStyle style = ToFontStyle(symbolicTraits.Value); + + results.Add(new NativeSystemFontFace( + familyName, + fontName, + path, + style, + GetStyleScore(weight, style), + faceIndex)); + } + finally + { + ReleaseIfNeeded(url); + } + } + + if (results.Count == 0) + { + return false; + } + + faces = [.. results]; + return true; + } + finally + { + ReleaseIfNeeded(descriptors); + ReleaseIfNeeded(collection); + } + } + + /// + /// Gets the CoreText system user-interface font family name. + /// + /// The font family name. + /// if CoreText returned a system user-interface font family name; otherwise, . + [SupportedOSPlatform("macos")] + private static bool TryGetDefaultFamilyNameMacOS([NotNullWhen(true)] out string? familyName) + { + familyName = null; + IntPtr font = CTFontCreateUIFontForLanguage(CTFontUIFontType.System, size: 0, language: IntPtr.Zero); + + try + { + if (font != IntPtr.Zero && TryGetManagedFamilyName(font, out familyName)) + { + return true; + } + + // Modern macOS can return a virtual UI face such as .AppleSystemUIFont which has no + // file-backed family. Use the first family which the managed collection can load so + // the returned name always satisfies the SystemFonts family-name contract. + NativeSystemFontFace[] faces = GetFamilyFacesMacOS(); + if (faces.Length > 0) + { + familyName = faces[0].FamilyName; + return true; + } + + return false; + } + finally + { + ReleaseIfNeeded(font); + } + } + + /// + /// Tries to match a character through CoreText on macOS. + /// + /// The code point to match. + /// The requested font style. + /// The requested family name. + /// The requested culture. + /// The matched family name. + /// The matched style. + /// if CoreText matched a system font; otherwise, . + [SupportedOSPlatform("macos")] + private static bool TryMatchCharacterMacOS( + CodePoint codePoint, + FontStyle style, + string? familyName, + CultureInfo? culture, + out string? matchedFamilyName, + out FontStyle matchedStyle) + { + matchedFamilyName = null; + matchedStyle = default; + + Span text = stackalloc char[2]; + text = text[..GetUtf16(codePoint, text)]; + string localeName = GetLocaleName(culture); + string? baseFamilyName = familyName; + + if (string.IsNullOrEmpty(baseFamilyName)) + { + _ = TryGetDefaultFamilyNameMacOS(out baseFamilyName); + } + + IntPtr textString = CreateString(text); + IntPtr localeString = CreateString(localeName); + IntPtr familyString = string.IsNullOrEmpty(baseFamilyName) + ? IntPtr.Zero + : CreateString(baseFamilyName); + IntPtr baseFont = IntPtr.Zero; + IntPtr fallbackFont = IntPtr.Zero; + + try + { + if (textString == IntPtr.Zero) + { + return false; + } + + if (familyString != IntPtr.Zero) + { + baseFont = CTFontCreateWithName(familyString, size: 0, matrix: IntPtr.Zero); + } + + if (baseFont == IntPtr.Zero) + { + baseFont = CTFontCreateUIFontForLanguage( + CTFontUIFontType.System, + size: 0, + localeString); + } + + if (baseFont == IntPtr.Zero) + { + return false; + } + + IntPtr styledBaseFont = CreateFontWithStyle(baseFont, style); + + if (styledBaseFont != IntPtr.Zero) + { + ReleaseIfNeeded(baseFont); + baseFont = styledBaseFont; + } + + fallbackFont = CTFontCreateForStringWithLanguage( + baseFont, + textString, + new CFRange(location: 0, length: text.Length), + localeString); + + if (fallbackFont == IntPtr.Zero || !HasGlyphs(fallbackFont, text)) + { + return false; + } + + // CoreText fallback returns a concrete face. Re-resolving the fallback family with + // the requested traits preserves the caller's bold/italic intent, matching Skia's + // CoreText fallback path. + if (TryGetFamilyName(fallbackFont, out string? fallbackFamilyName) + && fallbackFamilyName is { Length: > 0 } + && fallbackFamilyName[0] != '.') + { + IntPtr fallbackFamilyString = CreateString(fallbackFamilyName); + IntPtr styledFallbackBaseFont = IntPtr.Zero; + IntPtr styledFallbackFont = IntPtr.Zero; + + try + { + if (fallbackFamilyString != IntPtr.Zero) + { + styledFallbackBaseFont = CTFontCreateWithName(fallbackFamilyString, size: 0, matrix: IntPtr.Zero); + } + + if (styledFallbackBaseFont != IntPtr.Zero) + { + styledFallbackFont = CreateFontWithStyle(styledFallbackBaseFont, style); + + if (styledFallbackFont != IntPtr.Zero && HasGlyphs(styledFallbackFont, text)) + { + ReleaseIfNeeded(fallbackFont); + fallbackFont = styledFallbackFont; + styledFallbackFont = IntPtr.Zero; + } + } + } + finally + { + ReleaseIfNeeded(styledFallbackFont); + ReleaseIfNeeded(styledFallbackBaseFont); + ReleaseIfNeeded(fallbackFamilyString); + } + } + + if (!TryGetManagedFamilyName(fallbackFont, out matchedFamilyName)) + { + return false; + } + + matchedStyle = ToFontStyle(CTFontGetSymbolicTraits(fallbackFont)); + return true; + } + finally + { + ReleaseIfNeeded(fallbackFont); + ReleaseIfNeeded(baseFont); + ReleaseIfNeeded(familyString); + ReleaseIfNeeded(localeString); + ReleaseIfNeeded(textString); + } + } + + /// + /// Gets a string descriptor attribute. + /// + /// The CoreText font descriptor. + /// The descriptor attribute key. + /// The attribute value. + /// if the attribute was found and read; otherwise, . + private static bool TryGetDescriptorStringAttribute(IntPtr descriptor, IntPtr attribute, [NotNullWhen(true)] out string? value) + { + value = null; + IntPtr attributeValue = CTFontDescriptorCopyAttribute(descriptor, attribute); + + try + { + return attributeValue != IntPtr.Zero + && TryGetString(attributeValue, out value); + } + finally + { + ReleaseIfNeeded(attributeValue); + } + } + + /// + /// Gets a file-system path from a Core Foundation URL. + /// + /// The Core Foundation URL. + /// The file-system path. + /// if the path was read; otherwise, . + private static bool TryGetUrlPath(IntPtr url, [NotNullWhen(true)] out string? path) + { + path = null; + IntPtr pathString = CFURLCopyFileSystemPath(url, CFURLPathStyle.kCFURLPOSIXPathStyle); + + try + { + return pathString != IntPtr.Zero + && TryGetString(pathString, out path); + } + finally + { + ReleaseIfNeeded(pathString); + } + } + + /// + /// Gets the font collection face index for a file and PostScript name. + /// + /// The file-system path. + /// The CoreText font name. + /// The cached face indexes by path and font name. + /// The zero-based face index within the font file. + /// if the face index was found; otherwise, . + private static bool TryGetFaceIndex( + string path, + string fontName, + Dictionary> faceIndexesByPath, + out int faceIndex) + { + if (!faceIndexesByPath.TryGetValue(path, out Dictionary? faceIndexes)) + { + faceIndexes = new Dictionary(StringComparer.Ordinal); + + try + { + // CoreText descriptor order is not the OpenType collection order for virtual and + // localized faces. Name ID 6 is the stable identity shared by both APIs, so derive + // the managed face index from the file's own PostScript names. + string extension = Path.GetExtension(path); + if (extension.Equals(".ttc", StringComparison.OrdinalIgnoreCase) || extension.Equals(".otc", StringComparison.OrdinalIgnoreCase)) + { + ReadOnlySpan descriptions = FontDescription.LoadFontCollectionDescriptions(path).Span; + + for (int i = 0; i < descriptions.Length; i++) + { + string postScriptName = descriptions[i].GetNameById(CultureInfo.InvariantCulture, KnownNameIds.PostscriptName); + + if (postScriptName.Length > 0) + { + _ = faceIndexes.TryAdd(postScriptName, i); + } + } + } + else + { + FontDescription description = FontDescription.LoadDescription(path); + string postScriptName = description.GetNameById(CultureInfo.InvariantCulture, KnownNameIds.PostscriptName); + + if (postScriptName.Length > 0) + { + faceIndexes.Add(postScriptName, 0); + } + } + } + catch + { + // CoreText can expose protected or virtual faces whose URL is not a readable SFNT. + // Such a face cannot enter the managed collection's file-backed metrics. + } + + faceIndexesByPath.Add(path, faceIndexes); + } + + return faceIndexes.TryGetValue(fontName, out faceIndex); + } + + /// + /// Gets the cached CoreText faces that have a managed file representation. + /// + /// The managed CoreText faces. + [SupportedOSPlatform("macos")] + private static NativeSystemFontFace[] GetFamilyFacesMacOS() + { + NativeSystemFontFace[]? faces = Volatile.Read(ref cachedFamilyFaces); + + if (faces is not null) + { + return faces; + } + + lock (FamilyFacesLock) + { + faces = cachedFamilyFaces; + + if (faces is null) + { + faces = TryLoadFamilyFacesMacOS(out NativeSystemFontFace[]? loadedFaces) ? loadedFaces : []; + Dictionary familyNames = new(StringComparer.Ordinal); + + foreach (NativeSystemFontFace face in faces) + { + // CoreText fallback and UI-font APIs return a concrete PostScript face name, + // while the managed collection is keyed by family. Cache both identities so + // hidden aliases such as .AppleSystemUIFont resolve without reopening a font. + _ = familyNames.TryAdd(face.FaceName, face.FamilyName); + _ = familyNames.TryAdd(face.FamilyName, face.FamilyName); + } + + cachedFamilyNames = familyNames; + Volatile.Write(ref cachedFamilyFaces, faces); + } + } + + return faces; + } + + /// + /// Gets the managed family name for a CoreText font. + /// + /// The CoreText font. + /// The managed family name. + /// when the font maps to an enumerated managed family. + [SupportedOSPlatform("macos")] + private static bool TryGetManagedFamilyName(IntPtr font, [NotNullWhen(true)] out string? familyName) + { + _ = GetFamilyFacesMacOS(); + Dictionary familyNames = cachedFamilyNames!; + + if (TryGetFamilyName(font, out string? coreTextFamilyName) && familyNames.TryGetValue(coreTextFamilyName, out familyName)) + { + return true; + } + + if (TryGetPostScriptName(font, out string? coreTextFaceName) && familyNames.TryGetValue(coreTextFaceName, out familyName)) + { + return true; + } + + familyName = null; + return false; + } + + /// + /// Gets the weight and symbolic traits from a descriptor's traits dictionary with a + /// single attribute copy. + /// + /// The CoreText font descriptor. + /// The traits attribute key. + /// The weight trait key. + /// The symbolic trait key. + /// The CoreText weight value, or the regular weight when the descriptor does not expose one. + /// The symbolic traits, or when the descriptor does not expose them. + private static void GetDescriptorTraits( + IntPtr descriptor, + IntPtr traitsAttribute, + IntPtr weightTrait, + IntPtr symbolicTrait, + out double weight, + out uint? symbolicTraits) + { + weight = CTFontRegularWeight; + symbolicTraits = null; + + IntPtr traits = CTFontDescriptorCopyAttribute(descriptor, traitsAttribute); + + try + { + if (traits == IntPtr.Zero || CFGetTypeID(traits) != CFDictionaryGetTypeID()) + { + return; + } + + if (CFDictionaryGetValueIfPresent(traits, weightTrait, out IntPtr weightNumber) + && weightNumber != IntPtr.Zero + && CFGetTypeID(weightNumber) == CFNumberGetTypeID() + && CFNumberGetDoubleValue(weightNumber, CFNumberType.CGFloat, out double weightValue)) + { + weight = weightValue; + } + + if (CFDictionaryGetValueIfPresent(traits, symbolicTrait, out IntPtr symbolicNumber) + && symbolicNumber != IntPtr.Zero + && CFGetTypeID(symbolicNumber) == CFNumberGetTypeID() + && CFNumberGetIntValue(symbolicNumber, CFNumberType.SInt32, out int symbolicValue)) + { + symbolicTraits = unchecked((uint)symbolicValue); + } + } + finally + { + ReleaseIfNeeded(traits); + } + } + + /// + /// Gets a preference score for a CoreText face inside a Fonts style bucket. + /// + /// The CoreText weight value. + /// The Fonts style bucket. + /// The face preference score. + private static int GetStyleScore(double weight, FontStyle style) + { + double targetWeight = (style & FontStyle.Bold) == FontStyle.Bold + ? CTFontBoldWeight + : CTFontRegularWeight; + + return (int)(Math.Abs(weight - targetWeight) * CTFontStyleScoreScale); + } + + /// + /// Gets the CoreText locale name to use for fallback matching. + /// + /// The requested culture. + /// The locale name. + private static string GetLocaleName(CultureInfo? culture) + { + string localeName = culture?.Name ?? CultureInfo.CurrentCulture.Name; + + return localeName; + } + + /// + /// Creates a Core Foundation string. + /// + /// The string value. + /// The retained Core Foundation string. + private static unsafe IntPtr CreateString(string value) + { + fixed (char* valuePointer = value) + { + return CFStringCreateWithCharacters(IntPtr.Zero, valuePointer, value.Length); + } + } + + /// + /// Creates a Core Foundation string. + /// + /// The string value. + /// The retained Core Foundation string. + private static unsafe IntPtr CreateString(ReadOnlySpan value) + { + fixed (char* valuePointer = value) + { + return CFStringCreateWithCharacters(IntPtr.Zero, valuePointer, value.Length); + } + } + + /// + /// Encodes a code point into UTF-16. + /// + /// The code point to encode. + /// The UTF-16 output buffer. + /// The number of UTF-16 code units written. + private static int GetUtf16(CodePoint codePoint, Span buffer) + { + if (codePoint.IsBmp) + { + buffer[0] = (char)codePoint.Value; + return 1; + } + + int value = codePoint.Value - 0x10000; + buffer[0] = (char)((value >> 10) + 0xD800); + buffer[1] = (char)((value & 0x3FF) + 0xDC00); + return 2; + } + + /// + /// Gets whether a font maps every UTF-16 code unit in a string to a glyph. + /// + /// The CoreText font. + /// The text to test. + /// if every UTF-16 code unit maps to a glyph; otherwise, . + private static unsafe bool HasGlyphs(IntPtr font, ReadOnlySpan text) + { + Span glyphs = stackalloc ushort[text.Length]; + + fixed (char* charactersPointer = text) + { + fixed (ushort* glyphsPointer = glyphs) + { + return CTFontGetGlyphsForCharacters(font, (ushort*)charactersPointer, glyphsPointer, text.Length); + } + } + } + + /// + /// Gets a font family name. + /// + /// The CoreText font. + /// The family name. + /// if the family name was found; otherwise, . + private static bool TryGetFamilyName(IntPtr font, [NotNullWhen(true)] out string? familyName) + { + familyName = null; + IntPtr familyNameString = CTFontCopyFamilyName(font); + + try + { + return familyNameString != IntPtr.Zero + && TryGetString(familyNameString, out familyName); + } + finally + { + ReleaseIfNeeded(familyNameString); + } + } + + /// + /// Gets a font's PostScript face name. + /// + /// The CoreText font. + /// The PostScript face name. + /// if the face name was found; otherwise, . + private static bool TryGetPostScriptName(IntPtr font, [NotNullWhen(true)] out string? faceName) + { + faceName = null; + IntPtr faceNameString = CTFontCopyPostScriptName(font); + + try + { + return faceNameString != IntPtr.Zero && TryGetString(faceNameString, out faceName); + } + finally + { + ReleaseIfNeeded(faceNameString); + } + } + + /// + /// Gets a managed string from a Core Foundation string. + /// + /// The Core Foundation string. + /// The managed string. + /// if the string was read; otherwise, . + private static unsafe bool TryGetString(IntPtr value, [NotNullWhen(true)] out string? result) + { + result = null; + + int length = (int)CFStringGetLength(value); + + if (length == 0) + { + return false; + } + + result = string.Create( + length, + value, + static (buffer, source) => + { + fixed (char* bufferPointer = buffer) + { + CFStringGetCharacters(source, new CFRange(location: 0, length: buffer.Length), bufferPointer); + } + }); + + return true; + } + + /// + /// Creates a CoreText font copy with the requested Fonts style. + /// + /// The source CoreText font. + /// The requested Fonts style. + /// The retained CoreText font copy, or if CoreText cannot create one. + private static IntPtr CreateFontWithStyle(IntPtr font, FontStyle style) + { + const uint symbolicTraitMask = CTFontBoldTrait | CTFontItalicTrait; + uint symbolicTraits = ToCoreTextTraits(style); + + return CTFontCreateCopyWithSymbolicTraits( + font, + size: 0, + matrix: IntPtr.Zero, + symbolicTraits, + symbolicTraitMask); + } + + /// + /// Gets the CoreText symbolic traits for a Fonts style. + /// + /// The Fonts style. + /// The CoreText symbolic traits. + private static uint ToCoreTextTraits(FontStyle style) + { + uint result = 0; + + if ((style & FontStyle.Bold) == FontStyle.Bold) + { + result |= CTFontBoldTrait; + } + + if ((style & FontStyle.Italic) == FontStyle.Italic) + { + result |= CTFontItalicTrait; + } + + return result; + } + + /// + /// Gets the Fonts style for CoreText symbolic traits. + /// + /// The CoreText symbolic traits. + /// The Fonts style. + private static FontStyle ToFontStyle(uint traits) + { + FontStyle result = (traits & CTFontBoldTrait) != 0 ? FontStyle.Bold : FontStyle.Regular; + + if ((traits & CTFontItalicTrait) != 0) + { + result |= FontStyle.Italic; + } + + return result; + } + + /// + /// Releases a Core Foundation object when it exists. + /// + /// The Core Foundation object. + private static void ReleaseIfNeeded(IntPtr value) + { + if (value != IntPtr.Zero) + { + CFRelease(value); + } + } +} diff --git a/src/SixLabors.Fonts/Native/DirectWriteSystemFontMatcher.cs b/src/SixLabors.Fonts/Native/DirectWriteSystemFontMatcher.cs new file mode 100644 index 000000000..5f2d28dca --- /dev/null +++ b/src/SixLabors.Fonts/Native/DirectWriteSystemFontMatcher.cs @@ -0,0 +1,1713 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Runtime.InteropServices; +using System.Runtime.InteropServices.Marshalling; +using System.Runtime.Versioning; +using SixLabors.Fonts.Unicode; + +namespace SixLabors.Fonts.Native; + +/// +/// Provides Windows system font fallback matching through DirectWrite. +/// +/// +/// This is the Windows implementation of system fallback. The COM interface identifiers are the +/// DirectWrite interface identifiers declared by DWRITE_DECLARE_INTERFACE in the Windows SDK +/// dwrite.h and dwrite_2.h headers. The placeholder interface members are intentional: +/// COM dispatch is positional, so inherited DirectWrite vtable slots must be preserved even when this +/// type only calls the final fallback and metadata methods. +/// +internal static partial class DirectWriteSystemFontMatcher +{ + private const uint SpiGetNonClientMetrics = 0x0029; + private const int LfFaceSize = 32; + private static readonly Lazy LazyObjects = new(CreateObjects, isThreadSafe: true); + + /// + /// Defines the DirectWrite factory lifetime. + /// + internal enum DirectWriteFactoryType + { + /// + /// Reuses the shared DirectWrite factory. + /// + Shared = 0 + } + + /// + /// Defines the DirectWrite font weight values used by fallback matching. + /// + internal enum DirectWriteFontWeight + { + /// + /// Normal font weight. + /// + Normal = 400, + + /// + /// Semi-bold font weight. + /// + SemiBold = 600, + + /// + /// Bold font weight. + /// + Bold = 700 + } + + /// + /// Defines the DirectWrite font style values used by fallback matching. + /// + internal enum DirectWriteFontStyle + { + /// + /// Normal font style. + /// + Normal = 0, + + /// + /// Oblique font style. + /// + Oblique = 1, + + /// + /// Italic font style. + /// + Italic = 2 + } + + /// + /// Defines the DirectWrite font stretch values used by fallback matching. + /// + internal enum DirectWriteFontStretch + { + /// + /// Normal font stretch. + /// + Normal = 5 + } + + /// + /// Defines the paragraph reading direction supplied to DirectWrite. + /// + internal enum DirectWriteReadingDirection + { + /// + /// Left-to-right reading direction. + /// + LeftToRight = 0 + } + + /// + /// Supplies text and locale spans to DirectWrite text analysis APIs. + /// + /// + /// Windows SDK source: dwrite.h, IDWriteTextAnalysisSource, + /// IID 688e1a58-5094-47c8-adc8-fbcea60ae92b. + /// + [GeneratedComInterface] + [Guid("688E1A58-5094-47C8-ADC8-FBCEA60AE92B")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + internal partial interface IDWriteTextAnalysisSource + { + /// + /// Gets a text span at the requested position. + /// + /// The requested UTF-16 text position. + /// The returned pointer to the text span. + /// The returned length of the text span. + /// The operation result. + [PreserveSig] + public int GetTextAtPosition(uint textPosition, out IntPtr textString, out uint textLength); + + /// + /// Gets a text span before the requested position. + /// + /// The requested UTF-16 text position. + /// The returned pointer to the text span. + /// The returned length of the text span. + /// The operation result. + [PreserveSig] + public int GetTextBeforePosition(uint textPosition, out IntPtr textString, out uint textLength); + + /// + /// Gets the paragraph reading direction. + /// + /// The paragraph reading direction. + [PreserveSig] + public DirectWriteReadingDirection GetParagraphReadingDirection(); + + /// + /// Gets the locale name for the requested text position. + /// + /// The requested UTF-16 text position. + /// The returned length covered by the locale. + /// The returned pointer to the locale name. + /// The operation result. + [PreserveSig] + public int GetLocaleName(uint textPosition, out uint textLength, out IntPtr localeName); + + /// + /// Gets the number substitution for the requested text position. + /// + /// The requested UTF-16 text position. + /// The returned length covered by the substitution. + /// The returned number substitution pointer. + /// The operation result. + [PreserveSig] + public int GetNumberSubstitution(uint textPosition, out uint textLength, out IntPtr numberSubstitution); + } + + /// + /// Represents the DirectWrite factory interface needed to retrieve system font fallback. + /// + /// + /// Windows SDK source: dwrite_2.h, IDWriteFactory2, + /// IID 0439fc60-ca44-4994-8dee-3a9af7b732ec. The members before + /// preserve inherited IDWriteFactory and + /// IDWriteFactory1 vtable slots. + /// + [GeneratedComInterface] + [Guid("0439FC60-CA44-4994-8DEE-3A9AF7B732EC")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + internal partial interface IDWriteFactory2 + { + /// + /// Gets the system font collection. + /// + /// The returned system font collection. + /// Whether DirectWrite should check for updates to the system font collection. + /// The operation result. + [PreserveSig] + public int GetSystemFontCollection( + [MarshalAs(UnmanagedType.Interface)] out IDWriteFontCollection? fontCollection, + [MarshalAs(UnmanagedType.Bool)] bool checkForUpdates); + + /// + /// Preserves the DirectWrite IDWriteFactory::CreateCustomFontCollection vtable slot. + /// + /// The operation result. + [PreserveSig] + public int CreateCustomFontCollection(); + + /// + /// Preserves the DirectWrite IDWriteFactory::RegisterFontCollectionLoader vtable slot. + /// + /// The operation result. + [PreserveSig] + public int RegisterFontCollectionLoader(); + + /// + /// Preserves the DirectWrite IDWriteFactory::UnregisterFontCollectionLoader vtable slot. + /// + /// The operation result. + [PreserveSig] + public int UnregisterFontCollectionLoader(); + + /// + /// Preserves the DirectWrite IDWriteFactory::CreateFontFileReference vtable slot. + /// + /// The operation result. + [PreserveSig] + public int CreateFontFileReference(); + + /// + /// Preserves the DirectWrite IDWriteFactory::CreateCustomFontFileReference vtable slot. + /// + /// The operation result. + [PreserveSig] + public int CreateCustomFontFileReference(); + + /// + /// Preserves the DirectWrite IDWriteFactory::CreateFontFace vtable slot. + /// + /// The operation result. + [PreserveSig] + public int CreateFontFace(); + + /// + /// Preserves the DirectWrite IDWriteFactory::CreateRenderingParams vtable slot. + /// + /// The operation result. + [PreserveSig] + public int CreateRenderingParams(); + + /// + /// Preserves the DirectWrite IDWriteFactory::CreateMonitorRenderingParams vtable slot. + /// + /// The operation result. + [PreserveSig] + public int CreateMonitorRenderingParams(); + + /// + /// Preserves the DirectWrite IDWriteFactory::CreateCustomRenderingParams vtable slot. + /// + /// The operation result. + [PreserveSig] + public int CreateCustomRenderingParams(); + + /// + /// Preserves the DirectWrite IDWriteFactory::RegisterFontFileLoader vtable slot. + /// + /// The operation result. + [PreserveSig] + public int RegisterFontFileLoader(); + + /// + /// Preserves the DirectWrite IDWriteFactory::UnregisterFontFileLoader vtable slot. + /// + /// The operation result. + [PreserveSig] + public int UnregisterFontFileLoader(); + + /// + /// Preserves the DirectWrite IDWriteFactory::CreateTextFormat vtable slot. + /// + /// The operation result. + [PreserveSig] + public int CreateTextFormat(); + + /// + /// Preserves the DirectWrite IDWriteFactory::CreateTypography vtable slot. + /// + /// The operation result. + [PreserveSig] + public int CreateTypography(); + + /// + /// Preserves the DirectWrite IDWriteFactory::GetGdiInterop vtable slot. + /// + /// The operation result. + [PreserveSig] + public int GetGdiInterop(); + + /// + /// Preserves the DirectWrite IDWriteFactory::CreateTextLayout vtable slot. + /// + /// The operation result. + [PreserveSig] + public int CreateTextLayout(); + + /// + /// Preserves the DirectWrite IDWriteFactory::CreateGdiCompatibleTextLayout vtable slot. + /// + /// The operation result. + [PreserveSig] + public int CreateGdiCompatibleTextLayout(); + + /// + /// Preserves the DirectWrite IDWriteFactory::CreateEllipsisTrimmingSign vtable slot. + /// + /// The operation result. + [PreserveSig] + public int CreateEllipsisTrimmingSign(); + + /// + /// Preserves the DirectWrite IDWriteFactory::CreateTextAnalyzer vtable slot. + /// + /// The operation result. + [PreserveSig] + public int CreateTextAnalyzer(); + + /// + /// Preserves the DirectWrite IDWriteFactory::CreateNumberSubstitution vtable slot. + /// + /// The operation result. + [PreserveSig] + public int CreateNumberSubstitution(); + + /// + /// Preserves the DirectWrite IDWriteFactory::CreateGlyphRunAnalysis vtable slot. + /// + /// The operation result. + [PreserveSig] + public int CreateGlyphRunAnalysis(); + + /// + /// Preserves the DirectWrite IDWriteFactory1::GetEudcFontCollection vtable slot. + /// + /// The operation result. + [PreserveSig] + public int GetEudcFontCollection(); + + /// + /// Preserves the DirectWrite IDWriteFactory1::CreateCustomRenderingParams vtable slot. + /// + /// The operation result. + [PreserveSig] + public int CreateCustomRenderingParams1(); + + /// + /// Gets the system font fallback interface. + /// + /// The returned system font fallback interface. + /// The operation result. + [PreserveSig] + public int GetSystemFontFallback([MarshalAs(UnmanagedType.Interface)] out IDWriteFontFallback? fontFallback); + } + + /// + /// Represents a DirectWrite font collection. + /// + /// + /// Windows SDK source: dwrite.h, IDWriteFontCollection, + /// IID a84cee02-3eea-4eee-a827-87c1a02a0fcc. + /// + [GeneratedComInterface] + [Guid("A84CEE02-3EEA-4EEE-A827-87C1A02A0FCC")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + internal partial interface IDWriteFontCollection + { + /// + /// Gets the number of font families in the collection. + /// + /// The number of font families. + [PreserveSig] + public uint GetFontFamilyCount(); + + /// + /// Gets a font family by index. + /// + /// The family index. + /// The returned font family. + /// The operation result. + [PreserveSig] + public int GetFontFamily(uint index, [MarshalAs(UnmanagedType.Interface)] out IDWriteFontFamily? fontFamily); + + /// + /// Preserves the DirectWrite IDWriteFontCollection::FindFamilyName vtable slot. + /// + /// The operation result. + [PreserveSig] + public int FindFamilyName(); + + /// + /// Preserves the DirectWrite IDWriteFontCollection::GetFontFromFontFace vtable slot. + /// + /// The operation result. + [PreserveSig] + public int GetFontFromFontFace(); + } + + /// + /// Represents a DirectWrite local font file loader. + /// + /// + /// Windows SDK source: dwrite.h, IDWriteLocalFontFileLoader, + /// IID b2d9f3ec-c9fe-4a11-a2ec-d86208f7c0a2. The first method + /// preserves the inherited IDWriteFontFileLoader vtable slot. + /// + [GeneratedComInterface] + [Guid("B2D9F3EC-C9FE-4A11-A2EC-D86208F7C0A2")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + internal partial interface IDWriteLocalFontFileLoader + { + /// + /// Preserves the DirectWrite IDWriteFontFileLoader::CreateStreamFromKey vtable slot. + /// + /// The operation result. + [PreserveSig] + public int CreateStreamFromKey(); + + /// + /// Gets the local font file path length from a DirectWrite font file reference key. + /// + /// The font file reference key. + /// The font file reference key size, in bytes. + /// The file path length, excluding the null terminator. + /// The operation result. + [PreserveSig] + public int GetFilePathLengthFromKey(IntPtr fontFileReferenceKey, uint fontFileReferenceKeySize, out uint filePathLength); + + /// + /// Gets the local font file path from a DirectWrite font file reference key. + /// + /// The font file reference key. + /// The font file reference key size, in bytes. + /// The output file path buffer. + /// The output file path buffer size, including the null terminator. + /// The operation result. + [PreserveSig] + public unsafe int GetFilePathFromKey(IntPtr fontFileReferenceKey, uint fontFileReferenceKeySize, char* filePath, uint filePathSize); + } + + /// + /// Represents a DirectWrite font file. + /// + /// + /// Windows SDK source: dwrite.h, IDWriteFontFile, + /// IID 739d886a-cef5-47dc-8769-1a8b41bebbb0. + /// + [GeneratedComInterface] + [Guid("739D886A-CEF5-47DC-8769-1A8B41BEBBB0")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + internal partial interface IDWriteFontFile + { + /// + /// Gets the font file reference key. + /// + /// The returned font file reference key pointer. + /// The returned font file reference key size, in bytes. + /// The operation result. + [PreserveSig] + public int GetReferenceKey(out IntPtr fontFileReferenceKey, out uint fontFileReferenceKeySize); + + /// + /// Gets the local font file loader associated with the font file. + /// + /// The returned local font file loader. + /// The operation result. + [PreserveSig] + public int GetLoader([MarshalAs(UnmanagedType.Interface)] out IDWriteLocalFontFileLoader? fontFileLoader); + } + + /// + /// Represents a DirectWrite font face. + /// + /// + /// Windows SDK source: dwrite.h, IDWriteFontFace, + /// IID 5f49804d-7024-4d43-bfa9-d25984f53849. Only the file and index + /// methods are called; the preceding face type slot is preserved. + /// + [GeneratedComInterface] + [Guid("5F49804D-7024-4D43-BFA9-D25984F53849")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + internal partial interface IDWriteFontFace + { + /// + /// Preserves the DirectWrite IDWriteFontFace::GetType vtable slot. + /// + /// The font face type. + [PreserveSig] + public int GetType(); + + /// + /// Gets the font files representing this font face. + /// + /// The number of file entries. + /// The output font files. + /// The operation result. + [PreserveSig] + public int GetFiles( + ref uint numberOfFiles, + [Out, MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.Interface, SizeParamIndex = 0)] IDWriteFontFile?[]? fontFiles); + + /// + /// Gets the zero-based face index within the font file. + /// + /// The face index. + [PreserveSig] + public uint GetIndex(); + } + + /// + /// Represents the DirectWrite system font fallback interface. + /// + /// + /// Windows SDK source: dwrite_2.h, IDWriteFontFallback, + /// IID efa008f9-f7a1-48bf-b05c-f224713cc0ff. + /// + [GeneratedComInterface] + [Guid("EFA008F9-F7A1-48BF-B05C-F224713CC0FF")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + internal partial interface IDWriteFontFallback + { + /// + /// Maps text to a system fallback font. + /// + /// The text analysis source. + /// The starting UTF-16 text position. + /// The number of UTF-16 code units to map. + /// The base font collection pointer. + /// The requested base family name. + /// The requested base font weight. + /// The requested base font style. + /// The requested base font stretch. + /// The returned mapped text length. + /// The returned mapped font. + /// The returned font scale. + /// The operation result. + [PreserveSig] + public int MapCharacters( + IDWriteTextAnalysisSource analysisSource, + uint textPosition, + uint textLength, + IntPtr baseFontCollection, + [MarshalAs(UnmanagedType.LPWStr)] string? baseFamilyName, + DirectWriteFontWeight baseWeight, + DirectWriteFontStyle baseStyle, + DirectWriteFontStretch baseStretch, + out uint mappedLength, + [MarshalAs(UnmanagedType.Interface)] out IDWriteFont? mappedFont, + out float scale); + } + + /// + /// Represents a DirectWrite font. + /// + /// + /// Windows SDK source: dwrite.h, IDWriteFont, + /// IID acd16696-8c14-4f5d-877e-fe3fc1d32737. Members after + /// are retained to preserve the DirectWrite vtable shape. + /// + [GeneratedComInterface] + [Guid("ACD16696-8C14-4F5D-877E-FE3FC1D32737")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + internal partial interface IDWriteFont + { + /// + /// Gets the containing font family. + /// + /// The returned font family. + /// The operation result. + [PreserveSig] + public int GetFontFamily([MarshalAs(UnmanagedType.Interface)] out IDWriteFontFamily? fontFamily); + + /// + /// Gets the font weight. + /// + /// The font weight. + [PreserveSig] + public DirectWriteFontWeight GetWeight(); + + /// + /// Gets the font stretch. + /// + /// The font stretch. + [PreserveSig] + public DirectWriteFontStretch GetStretch(); + + /// + /// Gets the font style. + /// + /// The font style. + [PreserveSig] + public DirectWriteFontStyle GetStyle(); + + /// + /// Preserves the DirectWrite IDWriteFont::IsSymbolFont vtable slot. + /// + /// The operation result. + [PreserveSig] + public int IsSymbolFont(); + + /// + /// Preserves the DirectWrite IDWriteFont::GetFaceNames vtable slot. + /// + /// The operation result. + [PreserveSig] + public int GetFaceNames(); + + /// + /// Preserves the DirectWrite IDWriteFont::GetInformationalStrings vtable slot. + /// + /// The operation result. + [PreserveSig] + public int GetInformationalStrings(); + + /// + /// Preserves the DirectWrite IDWriteFont::GetSimulations vtable slot. + /// + /// The operation result. + [PreserveSig] + public int GetSimulations(); + + /// + /// Preserves the DirectWrite IDWriteFont::GetMetrics vtable slot. + /// + /// The operation result. + [PreserveSig] + public int GetMetrics(); + + /// + /// Preserves the DirectWrite IDWriteFont::HasCharacter vtable slot. + /// + /// The operation result. + [PreserveSig] + public int HasCharacter(); + + /// + /// Preserves the DirectWrite IDWriteFont::CreateFontFace vtable slot. + /// + /// The returned font face. + /// The operation result. + [PreserveSig] + public int CreateFontFace([MarshalAs(UnmanagedType.Interface)] out IDWriteFontFace? fontFace); + } + + /// + /// Represents a DirectWrite font family. + /// + /// + /// Windows SDK source: dwrite.h, IDWriteFontFamily, + /// IID da20d8ef-812a-4c43-9802-62ec4abd7add. The first three methods + /// preserve inherited IDWriteFontList vtable slots. + /// + [GeneratedComInterface] + [Guid("DA20D8EF-812A-4C43-9802-62EC4ABD7ADD")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + internal partial interface IDWriteFontFamily + { + /// + /// Preserves the DirectWrite IDWriteFontList::GetFontCollection vtable slot. + /// + /// The operation result. + [PreserveSig] + public int GetFontCollection(); + + /// + /// Gets the number of fonts in the family. + /// + /// The number of fonts in the family. + [PreserveSig] + public uint GetFontCount(); + + /// + /// Gets a font by index. + /// + /// The font index. + /// The returned font. + /// The operation result. + [PreserveSig] + public int GetFont(uint index, [MarshalAs(UnmanagedType.Interface)] out IDWriteFont? font); + + /// + /// Gets the localized family names. + /// + /// The returned localized family names. + /// The operation result. + [PreserveSig] + public int GetFamilyNames([MarshalAs(UnmanagedType.Interface)] out IDWriteLocalizedStrings? names); + + /// + /// Preserves the DirectWrite IDWriteFontFamily::GetFirstMatchingFont vtable slot. + /// + /// The operation result. + [PreserveSig] + public int GetFirstMatchingFont(); + + /// + /// Preserves the DirectWrite IDWriteFontFamily::GetMatchingFonts vtable slot. + /// + /// The operation result. + [PreserveSig] + public int GetMatchingFonts(); + } + + /// + /// Represents DirectWrite localized strings. + /// + /// + /// Windows SDK source: dwrite.h, IDWriteLocalizedStrings, + /// IID 08256209-099a-4b34-b86d-c22b110e7771. + /// + [GeneratedComInterface] + [Guid("08256209-099A-4B34-B86D-C22B110E7771")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + internal partial interface IDWriteLocalizedStrings + { + /// + /// Gets the number of localized strings. + /// + /// The number of localized strings. + [PreserveSig] + public uint GetCount(); + + /// + /// Finds a localized string by locale name. + /// + /// The locale name to find. + /// The returned string index. + /// The returned existence flag. + /// The operation result. + [PreserveSig] + public int FindLocaleName( + [MarshalAs(UnmanagedType.LPWStr)] string localeName, + out uint index, + out int exists); + + /// + /// Gets the locale name length. + /// + /// The string index. + /// The returned locale name length. + /// The operation result. + [PreserveSig] + public int GetLocaleNameLength(uint index, out uint length); + + /// + /// Gets the locale name. + /// + /// The string index. + /// The returned locale name. + /// The returned buffer size. + /// The operation result. + [PreserveSig] + public int GetLocaleName( + uint index, + [Out, MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.U2, SizeParamIndex = 2)] char[] localeName, + uint size); + + /// + /// Gets the localized string length. + /// + /// The string index. + /// The returned localized string length. + /// The operation result. + [PreserveSig] + public int GetStringLength(uint index, out uint length); + + /// + /// Gets the localized string. + /// + /// The string index. + /// The returned localized string. + /// The returned buffer size. + /// The operation result. + [PreserveSig] + public unsafe int GetString( + uint index, + char* value, + uint size); + } + + /// + /// Tries to get the Windows default font family name. + /// + /// The Windows default font family name. + /// if Windows returned a default font family name; otherwise, . + public static bool TryGetDefaultFamilyName([NotNullWhen(true)] out string? familyName) + { + familyName = null; + + if (!OperatingSystem.IsWindows()) + { + return false; + } + + try + { + return TryGetDefaultFamilyNameWindows(out familyName); + } + catch (DllNotFoundException) + { + return false; + } + catch (EntryPointNotFoundException) + { + return false; + } + } + + /// + /// Tries to match a character to a Windows system font. + /// + /// The code point to match. + /// The requested font style. + /// The requested family name. + /// The requested culture. + /// The matched family name. + /// The matched style. + /// if DirectWrite matched a system font; otherwise, . + public static bool TryMatchCharacter( + CodePoint codePoint, + FontStyle style, + string? familyName, + CultureInfo? culture, + out string? matchedFamilyName, + out FontStyle matchedStyle) + { + matchedFamilyName = null; + matchedStyle = default; + + if (!OperatingSystem.IsWindows()) + { + return false; + } + + return TryMatchCharacterWindows(codePoint, style, familyName, culture, out matchedFamilyName, out matchedStyle); + } + + /// + /// Tries to enumerate installed font family names through DirectWrite. + /// + /// Whether DirectWrite should check for updates to the system font collection. + /// The installed font family names. + /// if DirectWrite returned family names; otherwise, . + public static bool TryGetFamilyNames(bool checkForUpdates, [NotNullWhen(true)] out string[]? familyNames) + { + familyNames = null; + + if (!OperatingSystem.IsWindows()) + { + return false; + } + + return TryGetFamilyNamesWindows(checkForUpdates, out familyNames); + } + + /// + /// Tries to enumerate installed font faces through DirectWrite. + /// + /// Whether DirectWrite should check for updates to the system font collection. + /// The installed system font faces. + /// if DirectWrite returned font faces; otherwise, . + public static bool TryGetFamilyFaces(bool checkForUpdates, [NotNullWhen(true)] out NativeSystemFontFace[]? faces) + { + faces = null; + + if (!OperatingSystem.IsWindows()) + { + return false; + } + + return TryGetFamilyFacesWindows(checkForUpdates, out faces); + } + + /// + /// Tries to match a character through DirectWrite. + /// + /// The code point to match. + /// The requested font style. + /// The requested family name. + /// The requested culture. + /// The matched family name. + /// The matched style. + /// if DirectWrite matched a system font; otherwise, . + [SupportedOSPlatform("windows")] + private static unsafe bool TryMatchCharacterWindows( + CodePoint codePoint, + FontStyle style, + string? familyName, + CultureInfo? culture, + out string? matchedFamilyName, + out FontStyle matchedStyle) + { + matchedFamilyName = null; + matchedStyle = default; + + DirectWriteObjects? objects = LazyObjects.Value; + if (objects is null) + { + return false; + } + + Span text = stackalloc char[2]; + text = text[..GetUtf16(codePoint, text)]; + string localeName = GetLocaleName(culture); + string? baseFamilyName = string.IsNullOrEmpty(familyName) ? null : familyName; + + fixed (char* textPointer = text) + { + fixed (char* localeNamePointer = localeName) + { + TextAnalysisSource source = new(textPointer, (uint)text.Length, localeNamePointer); + + int result = objects.FontFallback.MapCharacters( + source, + textPosition: 0, + textLength: (uint)text.Length, + baseFontCollection: IntPtr.Zero, + baseFamilyName, + ToDirectWriteFontWeight(style), + ToDirectWriteFontStyle(style), + DirectWriteFontStretch.Normal, + out _, + out IDWriteFont? mappedFont, + out _); + + if (result < 0 || mappedFont is null) + { + return false; + } + + try + { + if (!TryGetFamilyName(mappedFont, localeName, out matchedFamilyName)) + { + return false; + } + + matchedStyle = ToFontStyle(mappedFont.GetWeight(), mappedFont.GetStyle()); + return true; + } + finally + { + DisposeComWrapper(mappedFont); + } + } + } + } + + /// + /// Tries to enumerate installed font family names through DirectWrite on Windows. + /// + /// Whether DirectWrite should check for updates to the system font collection. + /// The installed font family names. + /// if DirectWrite returned family names; otherwise, . + [SupportedOSPlatform("windows")] + private static bool TryGetFamilyNamesWindows(bool checkForUpdates, [NotNullWhen(true)] out string[]? familyNames) + { + familyNames = null; + + DirectWriteObjects? objects = LazyObjects.Value; + if (objects is null) + { + return false; + } + + IDWriteFontCollection? collection = null; + + try + { + if (objects.Factory.GetSystemFontCollection(out collection, checkForUpdates) < 0 || collection is null) + { + return false; + } + + int count = checked((int)collection.GetFontFamilyCount()); + string[] names = new string[count]; + string localeName = CultureInfo.CurrentUICulture.Name; + int nameCount = 0; + + for (int i = 0; i < count; i++) + { + IDWriteFontFamily? family = null; + IDWriteLocalizedStrings? localizedNames = null; + + try + { + if (collection.GetFontFamily((uint)i, out family) < 0 || family is null) + { + continue; + } + + if (family.GetFamilyNames(out localizedNames) < 0 || localizedNames is null) + { + continue; + } + + if ((TryGetLocalizedString(localizedNames, localeName, out string? familyName) + || TryGetString(localizedNames, index: 0, out familyName)) + && familyName is { Length: > 0 }) + { + names[nameCount++] = familyName; + } + } + finally + { + DisposeComWrapper(localizedNames); + DisposeComWrapper(family); + } + } + + if (nameCount == 0) + { + return false; + } + + if (nameCount != names.Length) + { + Array.Resize(ref names, nameCount); + } + + familyNames = names; + return true; + } + finally + { + DisposeComWrapper(collection); + } + } + + /// + /// Tries to enumerate installed font faces through DirectWrite on Windows. + /// + /// Whether DirectWrite should check for updates to the system font collection. + /// The installed system font faces. + /// if DirectWrite returned font faces; otherwise, . + [SupportedOSPlatform("windows")] + private static bool TryGetFamilyFacesWindows(bool checkForUpdates, [NotNullWhen(true)] out NativeSystemFontFace[]? faces) + { + faces = null; + + DirectWriteObjects? objects = LazyObjects.Value; + if (objects is null) + { + return false; + } + + IDWriteFontCollection? collection = null; + + try + { + if (objects.Factory.GetSystemFontCollection(out collection, checkForUpdates) < 0 || collection is null) + { + return false; + } + + uint familyCount = collection.GetFontFamilyCount(); + string localeName = CultureInfo.CurrentUICulture.Name; + List results = []; + + for (uint familyIndex = 0; familyIndex < familyCount; familyIndex++) + { + IDWriteFontFamily? family = null; + IDWriteLocalizedStrings? localizedNames = null; + + try + { + if (collection.GetFontFamily(familyIndex, out family) < 0 || family is null) + { + continue; + } + + if (family.GetFamilyNames(out localizedNames) < 0 || localizedNames is null) + { + continue; + } + + if ((!TryGetLocalizedString(localizedNames, localeName, out string? familyName) + && !TryGetString(localizedNames, index: 0, out familyName)) + || familyName is not { Length: > 0 }) + { + continue; + } + + uint fontCount = family.GetFontCount(); + + for (uint fontIndex = 0; fontIndex < fontCount; fontIndex++) + { + IDWriteFont? font = null; + + try + { + if (family.GetFont(fontIndex, out font) < 0 || font is null) + { + continue; + } + + if (TryGetFontFacePath(font, out string? path, out int faceIndex)) + { + DirectWriteFontWeight weight = font.GetWeight(); + FontStyle faceStyle = ToFontStyle(weight, font.GetStyle()); + + results.Add(new NativeSystemFontFace( + familyName, + familyName, + path, + faceStyle, + GetStyleScore(weight, faceStyle), + faceIndex)); + } + } + finally + { + DisposeComWrapper(font); + } + } + } + finally + { + DisposeComWrapper(localizedNames); + DisposeComWrapper(family); + } + } + + if (results.Count == 0) + { + return false; + } + + faces = [.. results]; + return true; + } + finally + { + DisposeComWrapper(collection); + } + } + + /// + /// Gets the Windows message font family name. + /// + /// The font family name. + /// if Windows returned a message font family name; otherwise, . + [SupportedOSPlatform("windows")] + private static bool TryGetDefaultFamilyNameWindows([NotNullWhen(true)] out string? familyName) + { + NonClientMetrics metrics = new() + { + Size = Marshal.SizeOf() + }; + + if (SystemParametersInfo(SpiGetNonClientMetrics, (uint)metrics.Size, ref metrics, 0)) + { + familyName = GetFaceName(metrics.MessageFont); + return familyName.Length > 0; + } + + familyName = null; + return false; + } + + /// + /// Gets the local font file path and face index for a DirectWrite font. + /// + /// The DirectWrite font. + /// The local font file path. + /// The zero-based face index within the font file. + /// if a local font file path was found; otherwise, . + [SupportedOSPlatform("windows")] + private static bool TryGetFontFacePath(IDWriteFont font, [NotNullWhen(true)] out string? path, out int faceIndex) + { + path = null; + faceIndex = 0; + IDWriteFontFace? fontFace = null; + + try + { + if (font.CreateFontFace(out fontFace) < 0 || fontFace is null) + { + return false; + } + + uint fileCount = 0; + if (fontFace.GetFiles(ref fileCount, null) < 0 || fileCount == 0) + { + return false; + } + + IDWriteFontFile?[] fontFiles = new IDWriteFontFile?[fileCount]; + uint requestedFileCount = fileCount; + + if (fontFace.GetFiles(ref requestedFileCount, fontFiles) < 0 || requestedFileCount == 0) + { + return false; + } + + faceIndex = checked((int)fontFace.GetIndex()); + + try + { + for (int i = 0; i < requestedFileCount; i++) + { + IDWriteFontFile? fontFile = fontFiles[i]; + if (fontFile is not null && TryGetFontFilePath(fontFile, out path)) + { + return true; + } + } + } + finally + { + for (int i = 0; i < fontFiles.Length; i++) + { + DisposeComWrapper(fontFiles[i]); + } + } + + return false; + } + finally + { + DisposeComWrapper(fontFace); + } + } + + /// + /// Gets the local path for a DirectWrite font file. + /// + /// The DirectWrite font file. + /// The local font file path. + /// if a local font file path was found; otherwise, . + [SupportedOSPlatform("windows")] + private static unsafe bool TryGetFontFilePath(IDWriteFontFile fontFile, [NotNullWhen(true)] out string? path) + { + path = null; + + if (fontFile.GetReferenceKey(out IntPtr referenceKey, out uint referenceKeySize) < 0 || referenceKey == IntPtr.Zero) + { + return false; + } + + IDWriteLocalFontFileLoader? loader; + try + { + if (fontFile.GetLoader(out loader) < 0 || loader is null) + { + return false; + } + } + catch (InvalidCastException) + { + // GetLoader returns IDWriteFontFileLoader; the marshaller queries it for + // IDWriteLocalFontFileLoader. Non-local loaders (remote or in-memory fonts) + // fail that query and have no file path, so the face is skipped. + return false; + } + + try + { + if (loader.GetFilePathLengthFromKey(referenceKey, referenceKeySize, out uint pathLength) < 0) + { + return false; + } + + using Buffer buffer = new(checked((int)pathLength + 1)); + Span pathBuffer = buffer.GetSpan()[..((int)pathLength + 1)]; + + fixed (char* pathPointer = pathBuffer) + { + if (loader.GetFilePathFromKey(referenceKey, referenceKeySize, pathPointer, (uint)pathBuffer.Length) < 0) + { + return false; + } + + path = new string(pathPointer, startIndex: 0, length: (int)pathLength); + return path.Length > 0; + } + } + finally + { + DisposeComWrapper(loader); + } + } + + /// + /// Creates the process-wide DirectWrite objects used by fallback matching. + /// + /// The DirectWrite objects, or when unavailable. + private static DirectWriteObjects? CreateObjects() + { + if (!OperatingSystem.IsWindows()) + { + return null; + } + + return CreateObjectsWindows(); + } + + /// + /// Creates the process-wide DirectWrite objects used by fallback matching on Windows. + /// + /// The DirectWrite objects, or when unavailable. + [SupportedOSPlatform("windows")] + private static DirectWriteObjects? CreateObjectsWindows() + { + Guid factoryId = new("0439FC60-CA44-4994-8DEE-3A9AF7B732EC"); + int result = DWriteCreateFactory(DirectWriteFactoryType.Shared, ref factoryId, out IDWriteFactory2? factory); + + if (result < 0 || factory is null) + { + return null; + } + + result = factory.GetSystemFontFallback(out IDWriteFontFallback? fontFallback); + if (result < 0 || fontFallback is null) + { + DisposeComWrapper(factory); + return null; + } + + return new DirectWriteObjects(factory, fontFallback); + } + + /// + /// Gets the DirectWrite locale name to use for fallback matching. + /// + /// The requested culture. + /// The locale name. + private static string GetLocaleName(CultureInfo? culture) + { + string localeName = culture?.Name ?? CultureInfo.CurrentCulture.Name; + + return localeName; + } + + /// + /// Gets the DirectWrite font weight for a Fonts style. + /// + /// The Fonts style. + /// The DirectWrite font weight. + private static DirectWriteFontWeight ToDirectWriteFontWeight(FontStyle style) + => (style & FontStyle.Bold) == FontStyle.Bold ? DirectWriteFontWeight.Bold : DirectWriteFontWeight.Normal; + + /// + /// Gets the DirectWrite font style for a Fonts style. + /// + /// The Fonts style. + /// The DirectWrite font style. + private static DirectWriteFontStyle ToDirectWriteFontStyle(FontStyle style) + => (style & FontStyle.Italic) == FontStyle.Italic ? DirectWriteFontStyle.Italic : DirectWriteFontStyle.Normal; + + /// + /// Gets the Fonts style for a DirectWrite weight and style. + /// + /// The DirectWrite font weight. + /// The DirectWrite font style. + /// The Fonts style. + private static FontStyle ToFontStyle(DirectWriteFontWeight weight, DirectWriteFontStyle style) + { + FontStyle result = weight >= DirectWriteFontWeight.SemiBold ? FontStyle.Bold : FontStyle.Regular; + + if (style is DirectWriteFontStyle.Italic or DirectWriteFontStyle.Oblique) + { + result |= FontStyle.Italic; + } + + return result; + } + + /// + /// Gets a preference score for a DirectWrite face inside a Fonts style bucket. + /// + /// The DirectWrite font weight. + /// The Fonts style bucket. + /// The face preference score. + private static int GetStyleScore(DirectWriteFontWeight weight, FontStyle style) + { + int targetWeight = (style & FontStyle.Bold) == FontStyle.Bold + ? (int)DirectWriteFontWeight.Bold + : (int)DirectWriteFontWeight.Normal; + + return Math.Abs((int)weight - targetWeight); + } + + /// + /// Gets the family name returned by DirectWrite for a font. + /// + /// The font. + /// The preferred locale name. + /// The family name. + /// if a family name was found; otherwise, . + [SupportedOSPlatform("windows")] + private static bool TryGetFamilyName(IDWriteFont font, string localeName, out string? familyName) + { + familyName = null; + IDWriteFontFamily? fontFamily = null; + IDWriteLocalizedStrings? familyNames = null; + + try + { + if (font.GetFontFamily(out fontFamily) < 0 || fontFamily is null) + { + return false; + } + + if (fontFamily.GetFamilyNames(out familyNames) < 0 || familyNames is null) + { + return false; + } + + return TryGetLocalizedString(familyNames, localeName, out familyName) + || TryGetString(familyNames, index: 0, out familyName); + } + finally + { + DisposeComWrapper(familyNames); + DisposeComWrapper(fontFamily); + } + } + + /// + /// Gets a localized string for the requested locale. + /// + /// The DirectWrite localized strings. + /// The locale name. + /// The localized string. + /// if a localized string was found; otherwise, . + private static bool TryGetLocalizedString(IDWriteLocalizedStrings strings, string localeName, out string? value) + { + value = null; + + if (strings.FindLocaleName(localeName, out uint index, out int exists) < 0 || exists == 0) + { + return false; + } + + return TryGetString(strings, index, out value); + } + + /// + /// Gets a localized string by index. + /// + /// The DirectWrite localized strings. + /// The string index. + /// The localized string. + /// if a localized string was found; otherwise, . + private static unsafe bool TryGetString(IDWriteLocalizedStrings strings, uint index, out string? value) + { + value = null; + + if (index >= strings.GetCount() || strings.GetStringLength(index, out uint length) < 0) + { + return false; + } + + using Buffer buffer = new((int)length + 1); + Span text = buffer.GetSpan()[..((int)length + 1)]; + + fixed (char* textPointer = text) + { + if (strings.GetString(index, textPointer, (uint)text.Length) < 0) + { + return false; + } + + value = new string(textPointer, startIndex: 0, length: (int)length); + return value.Length > 0; + } + } + + /// + /// Encodes a code point into UTF-16. + /// + /// The code point to encode. + /// The UTF-16 output buffer. + /// The number of UTF-16 code units written. + private static int GetUtf16(CodePoint codePoint, Span buffer) + { + if (codePoint.IsBmp) + { + buffer[0] = (char)codePoint.Value; + return 1; + } + + int value = codePoint.Value - 0x10000; + buffer[0] = (char)((value >> 10) + 0xD800); + buffer[1] = (char)((value & 0x3FF) + 0xDC00); + return 2; + } + + /// + /// Gets the face name from a Windows LOGFONTW. + /// + /// The font descriptor. + /// The face name. + private static unsafe string GetFaceName(LogFont font) + { + char* faceName = font.FaceName; + int length = 0; + + while (length < LfFaceSize && faceName[length] != '\0') + { + length++; + } + + return new string(faceName, startIndex: 0, length); + } + + /// + /// Marks the point at which a source-generated COM wrapper obtained from DirectWrite is + /// no longer used. The underlying COM references are released by the garbage collector. + /// + /// + /// Deterministic release is intentionally NOT performed. Source-generated COM wrappers + /// are ComWrappers-based instances: Marshal.ReleaseComObject is + /// invalid, IDisposable is not implemented, and is + /// a no-op on the runtime-cached wrappers the generated marshalling produces. Marshalling + /// with UniqueComInterfaceMarshaller to enable FinalRelease was tried and crashed with an + /// access violation on .NET 8: FinalRelease on a wrapper whose interface pointers were + /// cached by dynamic interface dispatch over-releases the native object, and unrelated + /// wrapper finalizers then release freed memory. Wrapper lifetime therefore follows the + /// garbage collector, which matches how the runtime intends cached wrappers to be used. + /// + /// The COM wrapper that is no longer used. + [SupportedOSPlatform("windows")] + private static void DisposeComWrapper(object? value) + => _ = value; + + /// + /// Creates a DirectWrite factory. + /// + /// + /// Windows SDK source: dwrite.h, DWriteCreateFactory. The requested + /// interface identifier is IDWriteFactory2 from dwrite_2.h. + /// + /// The factory type. + /// The requested factory interface identifier. + /// The returned factory interface. + /// The operation result. + [LibraryImport("dwrite.dll", EntryPoint = "DWriteCreateFactory")] + private static partial int DWriteCreateFactory( + DirectWriteFactoryType factoryType, + ref Guid iid, + [MarshalAs(UnmanagedType.Interface)] out IDWriteFactory2? factory); + + /// + /// Retrieves Windows system-wide parameters. + /// + /// The system parameter action. + /// The action parameter. + /// The non-client metrics. + /// The update flags. + /// if the system parameter was retrieved. + [LibraryImport("user32.dll", EntryPoint = "SystemParametersInfoW", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static partial bool SystemParametersInfo(uint action, uint parameter, ref NonClientMetrics metrics, uint update); + + /// + /// Contains Windows non-client area metrics. + /// + [StructLayout(LayoutKind.Sequential)] + private struct NonClientMetrics + { + public int Size; + public int BorderWidth; + public int ScrollWidth; + public int ScrollHeight; + public int CaptionWidth; + public int CaptionHeight; + public LogFont CaptionFont; + public int SmallCaptionWidth; + public int SmallCaptionHeight; + public LogFont SmallCaptionFont; + public int MenuWidth; + public int MenuHeight; + public LogFont MenuFont; + public LogFont StatusFont; + public LogFont MessageFont; + public int PaddedBorderWidth; + } + + /// + /// Contains Windows logical font attributes. + /// + [StructLayout(LayoutKind.Sequential)] + private unsafe struct LogFont + { + public int Height; + public int Width; + public int Escapement; + public int Orientation; + public int Weight; + public byte Italic; + public byte Underline; + public byte StrikeOut; + public byte CharSet; + public byte OutPrecision; + public byte ClipPrecision; + public byte Quality; + public byte PitchAndFamily; + public fixed char FaceName[LfFaceSize]; + } + + /// + /// Holds the DirectWrite interfaces reused for fallback matching. + /// + private sealed class DirectWriteObjects + { + /// + /// Initializes a new instance of the class. + /// + /// The DirectWrite factory. + /// The DirectWrite font fallback interface. + public DirectWriteObjects(IDWriteFactory2 factory, IDWriteFontFallback fontFallback) + { + this.Factory = factory; + this.FontFallback = fontFallback; + } + + /// + /// Gets the DirectWrite factory. + /// + public IDWriteFactory2 Factory { get; } + + /// + /// Gets the DirectWrite font fallback interface. + /// + public IDWriteFontFallback FontFallback { get; } + } + + /// + /// Supplies a single code point to DirectWrite text analysis. + /// + /// + /// DirectWrite fallback consumes text through IDWriteTextAnalysisSource rather than + /// directly accepting a scalar value, so this class exposes the one code point and its locale + /// through the callback interface expected by IDWriteFontFallback.MapCharacters. + /// + [GeneratedComClass] + internal sealed partial class TextAnalysisSource : IDWriteTextAnalysisSource + { + private const int Success = 0; + + private readonly IntPtr textPointer; + private readonly IntPtr localeNamePointer; + private readonly uint textLength; + + /// + /// Initializes a new instance of the class. + /// + /// The UTF-16 text to analyze. + /// The UTF-16 text length. + /// The locale name for the text. + public unsafe TextAnalysisSource(char* text, uint textLength, char* localeName) + { + this.textPointer = (IntPtr)text; + this.localeNamePointer = (IntPtr)localeName; + this.textLength = textLength; + } + + /// + public int GetTextAtPosition(uint textPosition, out IntPtr textString, out uint textLength) + { + if (textPosition >= this.textLength) + { + textString = IntPtr.Zero; + textLength = 0; + return Success; + } + + textString = IntPtr.Add(this.textPointer, (int)(textPosition * sizeof(char))); + textLength = this.textLength - textPosition; + return Success; + } + + /// + public int GetTextBeforePosition(uint textPosition, out IntPtr textString, out uint textLength) + { + if (textPosition == 0 || textPosition > this.textLength) + { + textString = IntPtr.Zero; + textLength = 0; + return Success; + } + + textString = this.textPointer; + textLength = textPosition; + return Success; + } + + /// + public DirectWriteReadingDirection GetParagraphReadingDirection() + => DirectWriteReadingDirection.LeftToRight; + + /// + public int GetLocaleName(uint textPosition, out uint textLength, out IntPtr localeName) + { + if (textPosition >= this.textLength) + { + textLength = 0; + localeName = IntPtr.Zero; + return Success; + } + + textLength = this.textLength - textPosition; + localeName = this.localeNamePointer; + return Success; + } + + /// + public int GetNumberSubstitution(uint textPosition, out uint textLength, out IntPtr numberSubstitution) + { + textLength = textPosition < this.textLength ? this.textLength - textPosition : 0; + numberSubstitution = IntPtr.Zero; + return Success; + } + } +} diff --git a/src/SixLabors.Fonts/Native/FontConfigSystemFontMatcher.cs b/src/SixLabors.Fonts/Native/FontConfigSystemFontMatcher.cs new file mode 100644 index 000000000..e707fba26 --- /dev/null +++ b/src/SixLabors.Fonts/Native/FontConfigSystemFontMatcher.cs @@ -0,0 +1,1058 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; +using SixLabors.Fonts.Unicode; + +namespace SixLabors.Fonts.Native; + +/// +/// Provides Linux system font fallback matching through Fontconfig. +/// +/// +/// This follows the same operating-system fallback shape as Skia's Fontconfig font manager: +/// build a pattern containing the requested family, style, language, and character set; let +/// Fontconfig select a matching installed font; then map the returned family and style back into +/// the managed system font collection. +/// +internal static partial class FontConfigSystemFontMatcher +{ + private const string FontConfigLibrary = "libfontconfig.so.1"; + private const string FamilyProperty = "family"; + private const string FileProperty = "file"; + private const string IndexProperty = "index"; + private const string CharSetProperty = "charset"; + private const string LanguageProperty = "lang"; + private const string WeightProperty = "weight"; + private const string SlantProperty = "slant"; + private const string WidthProperty = "width"; + private const int FcMatchPattern = 0; + private const int FcSetSystem = 0; + private const int FcSetApplication = 1; + private const int FcResultMatch = 0; + private const int FcResultNoId = 3; + private const int FcTypeString = 3; + private const int FcWeightNormal = 80; + private const int FcWeightDemiBold = 180; + private const int FcWeightBold = 200; + private const int FcSlantRoman = 0; + private const int FcSlantItalic = 100; + private const int FcWidthNormal = 100; + + /// + /// The first Fontconfig version (2.13.93) that locks internally. Older versions are not + /// thread safe and calls into them are serialized, matching Skia's FCLocker behavior. + /// + private const int FontConfigThreadSafeVersion = 21393; + + private static readonly Lazy LazyConfig = new(CreateConfig, isThreadSafe: true); + + /// + /// Serializes Fontconfig calls on library versions older than + /// . + /// + private static readonly object FontConfigLock = new(); + + /// + /// Whether the loaded Fontconfig version requires external locking. FcGetVersion has + /// always been thread safe. + /// + private static readonly Lazy LazyRequiresLock = new(() => FcGetVersion() < FontConfigThreadSafeVersion, isThreadSafe: true); + + /// + /// Tries to get the Linux default font family name. + /// + /// The Linux default font family name. + /// if Fontconfig returned a default font family name; otherwise, . + public static bool TryGetDefaultFamilyName([NotNullWhen(true)] out string? familyName) + { + familyName = null; + + if (!OperatingSystem.IsLinux()) + { + return false; + } + + try + { + return TryGetDefaultFamilyNameLinux(out familyName); + } + catch (DllNotFoundException) + { + return false; + } + catch (EntryPointNotFoundException) + { + return false; + } + } + + /// + /// Tries to enumerate installed font family names through Fontconfig. + /// + /// The installed font family names. + /// if Fontconfig returned family names; otherwise, . + public static bool TryGetFamilyNames([NotNullWhen(true)] out string[]? familyNames) + { + familyNames = null; + + if (!OperatingSystem.IsLinux()) + { + return false; + } + + try + { + return TryGetFamilyNamesLinux(out familyNames); + } + catch (DllNotFoundException) + { + return false; + } + catch (EntryPointNotFoundException) + { + return false; + } + } + + /// + /// Tries to enumerate installed system font faces through Fontconfig. + /// + /// The installed system font faces. + /// if Fontconfig returned font faces; otherwise, . + public static bool TryGetFamilyFaces([NotNullWhen(true)] out NativeSystemFontFace[]? faces) + { + faces = null; + + if (!OperatingSystem.IsLinux()) + { + return false; + } + + try + { + return TryGetFamilyFacesLinux(out faces); + } + catch (DllNotFoundException) + { + return false; + } + catch (EntryPointNotFoundException) + { + return false; + } + } + + /// + /// Tries to match a character through Fontconfig. + /// + /// The code point to match. + /// The requested font style. + /// The requested family name. + /// The requested culture. + /// The matched family name. + /// The matched style. + /// if Fontconfig matched a system font; otherwise, . + public static bool TryMatchCharacter( + CodePoint codePoint, + FontStyle style, + string? familyName, + CultureInfo? culture, + out string? matchedFamilyName, + out FontStyle matchedStyle) + { + matchedFamilyName = null; + matchedStyle = default; + + if (!OperatingSystem.IsLinux()) + { + return false; + } + + try + { + return TryMatchCharacterLinux(codePoint, style, familyName, culture, out matchedFamilyName, out matchedStyle); + } + catch (DllNotFoundException) + { + return false; + } + catch (EntryPointNotFoundException) + { + return false; + } + } + + /// + /// Tries to enumerate installed font family names through Fontconfig on Linux. + /// + /// The installed font family names. + /// if Fontconfig returned family names; otherwise, . + [SupportedOSPlatform("linux")] + private static bool TryGetFamilyNamesLinux([NotNullWhen(true)] out string[]? familyNames) + { + familyNames = null; + + IntPtr config = LazyConfig.Value; + if (config == IntPtr.Zero) + { + return false; + } + + bool lockTaken = false; + + try + { + EnterFontConfigLock(ref lockTaken); + HashSet names = new(StringComparer.Ordinal); + + // Fontconfig keeps fonts discovered from configuration files and fonts registered by + // applications in separate sets. Skia publishes the family names from both sets. + for (int setName = FcSetSystem; setName <= FcSetApplication; setName++) + { + IntPtr fontSetPointer = FcConfigGetFonts(config, setName); + if (fontSetPointer == IntPtr.Zero) + { + continue; + } + + FontConfigFontSet fontSet = Marshal.PtrToStructure(fontSetPointer); + + for (int i = 0; i < fontSet.FontCount; i++) + { + IntPtr pattern = Marshal.ReadIntPtr(fontSet.Fonts, i * IntPtr.Size); + + if (pattern == IntPtr.Zero) + { + continue; + } + + // A Fontconfig pattern can expose localized and alternate family names. Skia + // publishes every value so each platform-recognized name resolves to the face. + for (int familyId = 0; ; familyId++) + { + int result = FcPatternGetString(pattern, FamilyProperty, familyId, out IntPtr family); + if (result == FcResultNoId) + { + break; + } + + if (result != FcResultMatch || family == IntPtr.Zero) + { + continue; + } + + string? familyName = Marshal.PtrToStringUTF8(family); + + if (!string.IsNullOrEmpty(familyName)) + { + names.Add(familyName); + } + } + } + } + + if (names.Count == 0) + { + return false; + } + + familyNames = new string[names.Count]; + names.CopyTo(familyNames); + return true; + } + finally + { + ExitFontConfigLock(lockTaken); + } + } + + /// + /// Tries to enumerate installed system font faces through Fontconfig on Linux. + /// + /// The installed system font faces. + /// if Fontconfig returned font faces; otherwise, . + [SupportedOSPlatform("linux")] + private static bool TryGetFamilyFacesLinux([NotNullWhen(true)] out NativeSystemFontFace[]? faces) + { + faces = null; + + IntPtr config = LazyConfig.Value; + if (config == IntPtr.Zero) + { + return false; + } + + bool lockTaken = false; + + try + { + EnterFontConfigLock(ref lockTaken); + List results = []; + string? sysroot = Marshal.PtrToStringUTF8(FcConfigGetSysRoot(config)); + + // Keep face enumeration aligned with family enumeration so an application-set alias + // never appears without the file mapping required to load it. + for (int setName = FcSetSystem; setName <= FcSetApplication; setName++) + { + IntPtr fontSetPointer = FcConfigGetFonts(config, setName); + if (fontSetPointer == IntPtr.Zero) + { + continue; + } + + FontConfigFontSet fontSet = Marshal.PtrToStructure(fontSetPointer); + + for (int i = 0; i < fontSet.FontCount; i++) + { + IntPtr pattern = Marshal.ReadIntPtr(fontSet.Fonts, i * IntPtr.Size); + + if (pattern == IntPtr.Zero || !TryGetExistingFontPath(pattern, sysroot, out string? path)) + { + continue; + } + + int faceIndex = FcPatternGetInteger(pattern, IndexProperty, id: 0, out int index) == FcResultMatch + ? index + : 0; + + // Fontconfig uses the upper 16 bits for FreeType named instances and its variable- + // face sentinel. The managed loader accepts only a collection index and cannot + // reproduce those variation coordinates, so loading the lower index would select + // a different face from the one described by the Fontconfig pattern. + if ((faceIndex & ~0xFFFF) != 0) + { + continue; + } + + FontStyle style = ToFontStyle(pattern); + int styleScore = GetStyleScore(pattern, style); + + // Preserve every family value for the face. Fontconfig uses these values for + // localized and alternate names, and fallback can return any recognized family. + for (int familyId = 0; ; familyId++) + { + int result = FcPatternGetString(pattern, FamilyProperty, familyId, out IntPtr family); + if (result == FcResultNoId) + { + break; + } + + if (result != FcResultMatch || family == IntPtr.Zero) + { + continue; + } + + string? familyName = Marshal.PtrToStringUTF8(family); + if (!string.IsNullOrEmpty(familyName)) + { + results.Add(new NativeSystemFontFace(familyName, familyName, path, style, styleScore, faceIndex)); + } + } + } + } + + if (results.Count == 0) + { + return false; + } + + faces = [.. results]; + return true; + } + finally + { + ExitFontConfigLock(lockTaken); + } + } + + /// + /// Gets the default Fontconfig font family name. + /// + /// The font family name. + /// if Fontconfig returned a default font family name; otherwise, . + [SupportedOSPlatform("linux")] + private static bool TryGetDefaultFamilyNameLinux([NotNullWhen(true)] out string? familyName) + { + familyName = null; + + IntPtr config = LazyConfig.Value; + if (config == IntPtr.Zero) + { + return false; + } + + bool lockTaken = false; + IntPtr pattern = IntPtr.Zero; + IntPtr matchedPattern = IntPtr.Zero; + + try + { + EnterFontConfigLock(ref lockTaken); + pattern = FcPatternCreate(); + + if (pattern == IntPtr.Zero + || FcPatternAddInteger(pattern, WeightProperty, FcWeightNormal) == 0 + || FcPatternAddInteger(pattern, SlantProperty, FcSlantRoman) == 0 + || FcPatternAddInteger(pattern, WidthProperty, FcWidthNormal) == 0 + || FcConfigSubstitute(config, pattern, FcMatchPattern) == 0) + { + return false; + } + + FcDefaultSubstitute(pattern); + matchedPattern = FcFontMatch(config, pattern, out int matchResult); + + if (matchedPattern == IntPtr.Zero || matchResult != FcResultMatch) + { + return false; + } + + string? sysroot = Marshal.PtrToStringUTF8(FcConfigGetSysRoot(config)); + if (!TryGetExistingFontPath(matchedPattern, sysroot, out _)) + { + return false; + } + + if (FcPatternGetString(matchedPattern, FamilyProperty, id: 0, out IntPtr family) != FcResultMatch + || family == IntPtr.Zero) + { + return false; + } + + familyName = Marshal.PtrToStringUTF8(family); + return !string.IsNullOrEmpty(familyName); + } + finally + { + if (matchedPattern != IntPtr.Zero) + { + FcPatternDestroy(matchedPattern); + } + + if (pattern != IntPtr.Zero) + { + FcPatternDestroy(pattern); + } + + ExitFontConfigLock(lockTaken); + } + } + + /// + /// Tries to match a character through Fontconfig on Linux. + /// + /// The code point to match. + /// The requested font style. + /// The requested family name. + /// The requested culture. + /// The matched family name. + /// The matched style. + /// if Fontconfig matched a system font; otherwise, . + [SupportedOSPlatform("linux")] + private static bool TryMatchCharacterLinux( + CodePoint codePoint, + FontStyle style, + string? familyName, + CultureInfo? culture, + out string? matchedFamilyName, + out FontStyle matchedStyle) + { + matchedFamilyName = null; + matchedStyle = default; + + IntPtr config = LazyConfig.Value; + if (config == IntPtr.Zero) + { + return false; + } + + bool lockTaken = false; + IntPtr pattern = IntPtr.Zero; + IntPtr charSet = IntPtr.Zero; + IntPtr langSet = IntPtr.Zero; + IntPtr matchedPattern = IntPtr.Zero; + + try + { + EnterFontConfigLock(ref lockTaken); + pattern = FcPatternCreate(); + charSet = FcCharSetCreate(); + + if (pattern == IntPtr.Zero || charSet == IntPtr.Zero) + { + return false; + } + + // The requested family gets a weak binding so that character coverage outranks + // family loyalty during matching; with a strong binding the requested family can + // win the match even when it cannot render the character. Mirrors Skia. + if (!string.IsNullOrEmpty(familyName) + && !TryAddWeakFamily(pattern, familyName)) + { + return false; + } + + if (FcPatternAddInteger(pattern, WeightProperty, ToFontConfigWeight(style)) == 0 + || FcPatternAddInteger(pattern, SlantProperty, ToFontConfigSlant(style)) == 0 + || FcPatternAddInteger(pattern, WidthProperty, FcWidthNormal) == 0 + || FcCharSetAddChar(charSet, (uint)codePoint.Value) == 0 + || FcPatternAddCharSet(pattern, CharSetProperty, charSet) == 0) + { + return false; + } + + string localeName = GetLocaleName(culture); + if (!string.IsNullOrEmpty(localeName)) + { + langSet = FcLangSetCreate(); + if (langSet != IntPtr.Zero + && (FcLangSetAdd(langSet, localeName) == 0 + || FcPatternAddLangSet(pattern, LanguageProperty, langSet) == 0)) + { + return false; + } + } + + if (FcConfigSubstitute(config, pattern, FcMatchPattern) == 0) + { + return false; + } + + FcDefaultSubstitute(pattern); + matchedPattern = FcFontMatch(config, pattern, out int matchResult); + + if (matchedPattern == IntPtr.Zero || matchResult != FcResultMatch) + { + return false; + } + + // Fontconfig always returns some font; only accept it when it can actually + // render the requested character. Mirrors Skia's FontContainsCharacter. + if (!FontContainsCharacter(matchedPattern, (uint)codePoint.Value)) + { + return false; + } + + string? sysroot = Marshal.PtrToStringUTF8(FcConfigGetSysRoot(config)); + if (!TryGetExistingFontPath(matchedPattern, sysroot, out _)) + { + return false; + } + + if (FcPatternGetString(matchedPattern, FamilyProperty, id: 0, out IntPtr family) != FcResultMatch + || family == IntPtr.Zero) + { + return false; + } + + matchedFamilyName = Marshal.PtrToStringUTF8(family); + matchedStyle = ToFontStyle(matchedPattern); + return !string.IsNullOrEmpty(matchedFamilyName); + } + finally + { + if (matchedPattern != IntPtr.Zero) + { + FcPatternDestroy(matchedPattern); + } + + if (langSet != IntPtr.Zero) + { + FcLangSetDestroy(langSet); + } + + if (charSet != IntPtr.Zero) + { + FcCharSetDestroy(charSet); + } + + if (pattern != IntPtr.Zero) + { + FcPatternDestroy(pattern); + } + + ExitFontConfigLock(lockTaken); + } + } + + /// + /// Creates the process-wide Fontconfig configuration used by fallback matching. + /// + /// The Fontconfig configuration, or NULL when unavailable. + private static IntPtr CreateConfig() + { + if (!OperatingSystem.IsLinux()) + { + return IntPtr.Zero; + } + + bool lockTaken = false; + + try + { + EnterFontConfigLock(ref lockTaken); + return FcInitLoadConfigAndFonts(); + } + catch (DllNotFoundException) + { + return IntPtr.Zero; + } + catch (EntryPointNotFoundException) + { + return IntPtr.Zero; + } + finally + { + ExitFontConfigLock(lockTaken); + } + } + + /// + /// Enters the Fontconfig serialization lock when the loaded library version requires it. + /// + /// Set to when the lock was entered. + private static void EnterFontConfigLock(ref bool lockTaken) + { + if (LazyRequiresLock.Value) + { + Monitor.Enter(FontConfigLock, ref lockTaken); + } + } + + /// + /// Exits the Fontconfig serialization lock when it was entered. + /// + /// Whether the lock was entered. + private static void ExitFontConfigLock(bool lockTaken) + { + if (lockTaken) + { + Monitor.Exit(FontConfigLock); + } + } + + /// + /// Adds a family name to a pattern with a weak binding, so that character coverage + /// outranks family loyalty during matching. Fontconfig copies the value. + /// + /// The pattern. + /// The family name. + /// Non-zero on success; otherwise, zero. + private static bool TryAddWeakFamily(IntPtr pattern, string familyName) + { + IntPtr utf8 = Marshal.StringToCoTaskMemUTF8(familyName); + + try + { + FontConfigValue value = new() + { + Type = FcTypeString, + Value = utf8 + }; + + return FcPatternAddWeak(pattern, FamilyProperty, value, append: 0) != 0; + } + finally + { + Marshal.FreeCoTaskMem(utf8); + } + } + + /// + /// Gets whether a matched font pattern can render the requested character, checking + /// every character set on the pattern. + /// + /// The matched font pattern. + /// The Unicode code point. + /// if a character set contains the code point. + private static bool FontContainsCharacter(IntPtr font, uint character) + { + for (int charSetId = 0; ; charSetId++) + { + int result = FcPatternGetCharSet(font, CharSetProperty, charSetId, out IntPtr charSet); + if (result == FcResultNoId) + { + return false; + } + + if (result != FcResultMatch) + { + continue; + } + + if (charSet != IntPtr.Zero && FcCharSetHasChar(charSet, character) != 0) + { + return true; + } + } + } + + /// + /// Resolves the existing file represented by a Fontconfig pattern. + /// + /// The Fontconfig pattern. + /// The configured filesystem root. + /// The existing font path. + /// when either the sysroot-relative or original path exists. + private static bool TryGetExistingFontPath(IntPtr pattern, string? sysroot, [NotNullWhen(true)] out string? path) + { + path = null; + + if (FcPatternGetString(pattern, FileProperty, id: 0, out IntPtr file) != FcResultMatch || file == IntPtr.Zero) + { + return false; + } + + string? configuredPath = Marshal.PtrToStringUTF8(file); + if (string.IsNullOrEmpty(configuredPath)) + { + return false; + } + + // Fontconfig can contain a mix of sysroot-relative and ordinary paths. Skia tries the + // sysroot-prefixed path first, then preserves compatibility with application-added files + // outside that root by trying the original path. + if (!string.IsNullOrEmpty(sysroot)) + { + string resolvedPath = string.Concat(sysroot, configuredPath); + if (File.Exists(resolvedPath)) + { + path = resolvedPath; + return true; + } + } + + if (File.Exists(configuredPath)) + { + path = configuredPath; + return true; + } + + return false; + } + + /// + /// Gets the Fontconfig locale name to use for fallback matching. + /// + /// The requested culture. + /// The locale name. + private static string GetLocaleName(CultureInfo? culture) + { + string localeName = culture?.Name ?? CultureInfo.CurrentCulture.Name; + + return localeName; + } + + /// + /// Gets the Fontconfig weight for a Fonts style. + /// + /// The Fonts style. + /// The Fontconfig weight. + private static int ToFontConfigWeight(FontStyle style) + => (style & FontStyle.Bold) == FontStyle.Bold ? FcWeightBold : FcWeightNormal; + + /// + /// Gets the Fontconfig slant for a Fonts style. + /// + /// The Fonts style. + /// The Fontconfig slant. + private static int ToFontConfigSlant(FontStyle style) + => (style & FontStyle.Italic) == FontStyle.Italic ? FcSlantItalic : FcSlantRoman; + + /// + /// Gets the Fonts style for a Fontconfig pattern. + /// + /// The Fontconfig pattern. + /// The Fonts style. + private static FontStyle ToFontStyle(IntPtr pattern) + { + FontStyle result = FontStyle.Regular; + + if (FcPatternGetInteger(pattern, WeightProperty, id: 0, out int weight) == FcResultMatch + && weight >= FcWeightDemiBold) + { + result |= FontStyle.Bold; + } + + if (FcPatternGetInteger(pattern, SlantProperty, id: 0, out int slant) == FcResultMatch + && slant != FcSlantRoman) + { + result |= FontStyle.Italic; + } + + return result; + } + + /// + /// Gets a preference score for a Fontconfig face inside a Fonts style bucket. + /// + /// The Fontconfig pattern. + /// The Fonts style bucket. + /// The face preference score. + private static int GetStyleScore(IntPtr pattern, FontStyle style) + { + int score = 0; + + if (FcPatternGetInteger(pattern, WeightProperty, id: 0, out int weight) == FcResultMatch) + { + int targetWeight = (style & FontStyle.Bold) == FontStyle.Bold ? FcWeightBold : FcWeightNormal; + score += Math.Abs(weight - targetWeight); + } + + if (FcPatternGetInteger(pattern, SlantProperty, id: 0, out int slant) == FcResultMatch) + { + score += Math.Abs(slant - ToFontConfigSlant(style)); + } + + return score; + } + + /// + /// Initializes Fontconfig and loads the system font configuration. + /// + /// The loaded Fontconfig configuration. + [LibraryImport(FontConfigLibrary)] + private static partial IntPtr FcInitLoadConfigAndFonts(); + + /// + /// Gets a Fontconfig font set owned by the configuration. + /// + /// The Fontconfig configuration. + /// The requested font set. + /// The font set pointer. + [LibraryImport(FontConfigLibrary)] + private static partial IntPtr FcConfigGetFonts(IntPtr config, int setName); + + /// + /// Gets the filesystem root applied to font paths in the configuration. + /// + /// The Fontconfig configuration. + /// The borrowed sysroot string, or NULL when no sysroot is configured. + [LibraryImport(FontConfigLibrary)] + private static partial IntPtr FcConfigGetSysRoot(IntPtr config); + + /// + /// Creates an empty font pattern. + /// + /// The created pattern. + [LibraryImport(FontConfigLibrary)] + private static partial IntPtr FcPatternCreate(); + + /// + /// Destroys a font pattern. + /// + /// The pattern to destroy. + [LibraryImport(FontConfigLibrary)] + private static partial void FcPatternDestroy(IntPtr pattern); + + /// + /// Gets the Fontconfig library version as a single integer + /// (major * 10000 + minor * 100 + revision). + /// + /// The Fontconfig version. + [LibraryImport(FontConfigLibrary)] + private static partial int FcGetVersion(); + + /// + /// Adds a value to a font pattern with a weak binding. Weakly bound values rank below + /// strongly bound ones during matching, letting other criteria such as character + /// coverage take precedence. + /// + /// The pattern. + /// The object name. + /// The value to add. Fontconfig stores a copy. + /// Non-zero to append the value after existing values. + /// Non-zero on success; otherwise, zero. + [LibraryImport(FontConfigLibrary, StringMarshalling = StringMarshalling.Utf8)] + private static partial int FcPatternAddWeak(IntPtr pattern, string name, FontConfigValue value, int append); + + /// + /// Adds an integer value to a font pattern. + /// + /// The pattern. + /// The object name. + /// The integer value. + /// Non-zero on success; otherwise, zero. + [LibraryImport(FontConfigLibrary, StringMarshalling = StringMarshalling.Utf8)] + private static partial int FcPatternAddInteger(IntPtr pattern, string name, int value); + + /// + /// Adds a character set to a font pattern. + /// + /// The pattern. + /// The object name. + /// The character set. + /// Non-zero on success; otherwise, zero. + [LibraryImport(FontConfigLibrary, StringMarshalling = StringMarshalling.Utf8)] + private static partial int FcPatternAddCharSet(IntPtr pattern, string name, IntPtr charSet); + + /// + /// Adds a language set to a font pattern. + /// + /// The pattern. + /// The object name. + /// The language set. + /// Non-zero on success; otherwise, zero. + [LibraryImport(FontConfigLibrary, StringMarshalling = StringMarshalling.Utf8)] + private static partial int FcPatternAddLangSet(IntPtr pattern, string name, IntPtr langSet); + + /// + /// Gets a string value from a font pattern. + /// + /// The pattern. + /// The object name. + /// The value index. + /// The string value. + /// The Fontconfig result. + [LibraryImport(FontConfigLibrary, StringMarshalling = StringMarshalling.Utf8)] + private static partial int FcPatternGetString(IntPtr pattern, string name, int id, out IntPtr value); + + /// + /// Gets an integer value from a font pattern. + /// + /// The pattern. + /// The object name. + /// The value index. + /// The integer value. + /// The Fontconfig result. + [LibraryImport(FontConfigLibrary, StringMarshalling = StringMarshalling.Utf8)] + private static partial int FcPatternGetInteger(IntPtr pattern, string name, int id, out int value); + + /// + /// Creates an empty character set. + /// + /// The created character set. + [LibraryImport(FontConfigLibrary)] + private static partial IntPtr FcCharSetCreate(); + + /// + /// Destroys a character set. + /// + /// The character set to destroy. + [LibraryImport(FontConfigLibrary)] + private static partial void FcCharSetDestroy(IntPtr charSet); + + /// + /// Adds a Unicode code point to a character set. + /// + /// The character set. + /// The Unicode code point. + /// Non-zero on success; otherwise, zero. + [LibraryImport(FontConfigLibrary)] + private static partial int FcCharSetAddChar(IntPtr charSet, uint codePoint); + + /// + /// Gets whether a character set contains a Unicode code point. + /// + /// The character set. + /// The Unicode code point. + /// Non-zero when the code point is present; otherwise, zero. + [LibraryImport(FontConfigLibrary)] + private static partial int FcCharSetHasChar(IntPtr charSet, uint codePoint); + + /// + /// Gets a character set from a font pattern without transferring ownership. + /// + /// The pattern. + /// The object name. + /// The value index. + /// The character set, owned by the pattern. + /// The Fontconfig result. + [LibraryImport(FontConfigLibrary, StringMarshalling = StringMarshalling.Utf8)] + private static partial int FcPatternGetCharSet(IntPtr pattern, string name, int id, out IntPtr charSet); + + /// + /// Creates an empty language set. + /// + /// The created language set. + [LibraryImport(FontConfigLibrary)] + private static partial IntPtr FcLangSetCreate(); + + /// + /// Destroys a language set. + /// + /// The language set to destroy. + [LibraryImport(FontConfigLibrary)] + private static partial void FcLangSetDestroy(IntPtr langSet); + + /// + /// Adds a language tag to a language set. + /// + /// The language set. + /// The language tag. + /// Non-zero on success; otherwise, zero. + [LibraryImport(FontConfigLibrary, StringMarshalling = StringMarshalling.Utf8)] + private static partial int FcLangSetAdd(IntPtr langSet, string language); + + /// + /// Performs Fontconfig substitutions against a pattern. + /// + /// The Fontconfig configuration. + /// The pattern. + /// The match kind. + /// Non-zero on success; otherwise, zero. + [LibraryImport(FontConfigLibrary)] + private static partial int FcConfigSubstitute(IntPtr config, IntPtr pattern, int kind); + + /// + /// Performs default substitutions against a pattern. + /// + /// The pattern. + [LibraryImport(FontConfigLibrary)] + private static partial void FcDefaultSubstitute(IntPtr pattern); + + /// + /// Finds the best matching font for a pattern. + /// + /// The Fontconfig configuration. + /// The pattern. + /// The match result. + /// The matched pattern. + [LibraryImport(FontConfigLibrary)] + private static partial IntPtr FcFontMatch(IntPtr config, IntPtr pattern, out int result); + + /// + /// Represents the Fontconfig FcValue layout: an FcType discriminant followed + /// by a pointer-sized union. The union starts at offset 8 on 64-bit platforms because it + /// is pointer aligned. + /// + [StructLayout(LayoutKind.Sequential)] + private struct FontConfigValue + { + /// + /// The FcType discriminant of . + /// + public int Type; + + /// + /// The union payload; a native string pointer for FcTypeString. + /// + public IntPtr Value; + } + + /// + /// Represents the Fontconfig FcFontSet layout. + /// + [StructLayout(LayoutKind.Sequential)] + private readonly struct FontConfigFontSet + { + /// + /// Gets the number of font patterns in the set. + /// + public readonly int FontCount; + + /// + /// Gets the allocated font pattern capacity. + /// + public readonly int Size; + + /// + /// Gets the native array of font pattern pointers. + /// + public readonly IntPtr Fonts; + } +} diff --git a/src/SixLabors.Fonts/Native/MacSystemFontsEnumerator.cs b/src/SixLabors.Fonts/Native/MacSystemFontsEnumerator.cs deleted file mode 100644 index 9f28aff6b..000000000 --- a/src/SixLabors.Fonts/Native/MacSystemFontsEnumerator.cs +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Buffers; -using System.Collections; -using System.Diagnostics; -using System.Text; -using static SixLabors.Fonts.Native.CoreFoundation; -using static SixLabors.Fonts.Native.CoreText; - -namespace SixLabors.Fonts.Native; - -/// -/// An enumerator that enumerates over available macOS system fonts. -/// The enumerated strings are the absolute paths to the font files. -/// -/// -/// Internally, it calls the native CoreText's method to retrieve -/// the list of fonts so using this class must be guarded by RuntimeInformation.IsOSPlatform(OSPlatform.OSX). -/// -internal sealed class MacSystemFontsEnumerator : IEnumerable, IEnumerator -{ - private static readonly ArrayPool BytePool = ArrayPool.Shared; - - private readonly IntPtr fontUrls; - private readonly bool releaseFontUrls; - private int fontIndex; - - public MacSystemFontsEnumerator() - : this(CTFontManagerCopyAvailableFontURLs(), releaseFontUrls: true, fontIndex: 0) - { - } - - private MacSystemFontsEnumerator(IntPtr fontUrls, bool releaseFontUrls, int fontIndex) - { - if (fontUrls == IntPtr.Zero) - { - throw new ArgumentException($"The {nameof(fontUrls)} must not be NULL.", nameof(fontUrls)); - } - - this.fontUrls = fontUrls; - this.releaseFontUrls = releaseFontUrls; - this.fontIndex = fontIndex; - - this.Current = null!; - } - - public string Current { get; private set; } - - object IEnumerator.Current => this.Current; - - public bool MoveNext() - { - Debug.Assert(CFGetTypeID(this.fontUrls) == CFArrayGetTypeID(), "The fontUrls array must be a CFArrayRef"); - if (this.fontIndex < CFArrayGetCount(this.fontUrls)) - { - IntPtr fontUrl = CFArrayGetValueAtIndex(this.fontUrls, this.fontIndex); - Debug.Assert(CFGetTypeID(fontUrl) == CFURLGetTypeID(), "The elements of the fontUrls array must be a CFURLRef"); - IntPtr fontPath = CFURLCopyFileSystemPath(fontUrl, CFURLPathStyle.kCFURLPOSIXPathStyle); - - int fontPathLength = (int)CFStringGetLength(fontPath); - int fontPathBufferSize = (fontPathLength + 1) * 2; // +1 for the NULL byte and *2 for UTF-16 - byte[] fontPathBuffer = BytePool.Rent(fontPathBufferSize); - CFStringGetCString(fontPath, fontPathBuffer, fontPathBufferSize, CFStringEncoding.kCFStringEncodingUTF16LE); - this.Current = Encoding.Unicode.GetString(fontPathBuffer, 0, fontPathBufferSize - 2); // -2 for the UTF-16 NULL - BytePool.Return(fontPathBuffer); - - CFRelease(fontPath); - - this.fontIndex++; - - return true; - } - - return false; - } - - public void Reset() => this.fontIndex = 0; - - public void Dispose() - { - if (this.releaseFontUrls) - { - CFRelease(this.fontUrls); - } - } - - public IEnumerator GetEnumerator() => new MacSystemFontsEnumerator(this.fontUrls, releaseFontUrls: false, this.fontIndex); - - IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); -} diff --git a/src/SixLabors.Fonts/Native/NativeSystemFontFace.cs b/src/SixLabors.Fonts/Native/NativeSystemFontFace.cs new file mode 100644 index 000000000..0611f1ffd --- /dev/null +++ b/src/SixLabors.Fonts/Native/NativeSystemFontFace.cs @@ -0,0 +1,59 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Native; + +/// +/// Represents one platform family face resolved to a local font file. +/// +internal readonly struct NativeSystemFontFace +{ + /// + /// Initializes a new instance of the struct. + /// + /// The platform family name. + /// The platform face name. + /// The local font file path. + /// The platform font style. + /// The platform-specific preference score for the font style. + /// The zero-based face index within the font file. + public NativeSystemFontFace(string familyName, string faceName, string path, FontStyle style, int styleScore, int faceIndex) + { + this.FamilyName = familyName; + this.FaceName = faceName; + this.Path = path; + this.Style = style; + this.StyleScore = styleScore; + this.FaceIndex = faceIndex; + } + + /// + /// Gets the platform family name. + /// + public string FamilyName { get; } + + /// + /// Gets the platform face name used to identify a specific font inside its family. + /// + public string FaceName { get; } + + /// + /// Gets the local font file path. + /// + public string Path { get; } + + /// + /// Gets the platform font style. + /// + public FontStyle Style { get; } + + /// + /// Gets the platform-specific preference score for the font style. + /// + public int StyleScore { get; } + + /// + /// Gets the zero-based face index within the font file. + /// + public int FaceIndex { get; } +} diff --git a/src/SixLabors.Fonts/Native/SystemFontMatcher.cs b/src/SixLabors.Fonts/Native/SystemFontMatcher.cs new file mode 100644 index 000000000..a10a62846 --- /dev/null +++ b/src/SixLabors.Fonts/Native/SystemFontMatcher.cs @@ -0,0 +1,150 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using SixLabors.Fonts.Unicode; + +namespace SixLabors.Fonts.Native; + +/// +/// Dispatches system character matching to the current operating system font service. +/// +internal static class SystemFontMatcher +{ + /// + /// Tries to get the operating system default font family name. + /// + /// The operating system default font family name. + /// if the operating system returned a default font family name; otherwise, . + public static bool TryGetDefaultFamilyName([NotNullWhen(true)] out string? familyName) + { + if (OperatingSystem.IsWindows()) + { + return DirectWriteSystemFontMatcher.TryGetDefaultFamilyName(out familyName); + } + + if (OperatingSystem.IsMacOS()) + { + return CoreTextSystemFontMatcher.TryGetDefaultFamilyName(out familyName); + } + + if (OperatingSystem.IsLinux()) + { + return FontConfigSystemFontMatcher.TryGetDefaultFamilyName(out familyName); + } + + familyName = null; + return false; + } + + /// + /// Tries to enumerate installed system font family names. + /// + /// Whether the platform should check for updates to the system font collection. + /// The installed font family names. + /// if the operating system returned family names; otherwise, . + public static bool TryGetFamilyNames(bool checkForUpdates, [NotNullWhen(true)] out string[]? familyNames) + { + if (OperatingSystem.IsWindows()) + { + return DirectWriteSystemFontMatcher.TryGetFamilyNames(checkForUpdates, out familyNames); + } + + if (OperatingSystem.IsMacOS()) + { + return CoreTextSystemFontMatcher.TryGetFamilyNames(out familyNames); + } + + if (OperatingSystem.IsLinux()) + { + return FontConfigSystemFontMatcher.TryGetFamilyNames(out familyNames); + } + + familyNames = null; + return false; + } + + /// + /// Tries to enumerate installed system font faces resolved to platform family names. + /// + /// Whether the platform should check for updates to the system font collection. + /// The installed system font faces. + /// if the operating system returned font faces; otherwise, . + public static bool TryGetFamilyFaces(bool checkForUpdates, [NotNullWhen(true)] out NativeSystemFontFace[]? faces) + { + if (OperatingSystem.IsWindows()) + { + return DirectWriteSystemFontMatcher.TryGetFamilyFaces(checkForUpdates, out faces); + } + + if (OperatingSystem.IsMacOS()) + { + return CoreTextSystemFontMatcher.TryGetFamilyFaces(out faces); + } + + if (OperatingSystem.IsLinux()) + { + return FontConfigSystemFontMatcher.TryGetFamilyFaces(out faces); + } + + faces = null; + return false; + } + + /// + /// Tries to match a character to an installed system font. + /// + /// The code point to match. + /// The requested font style. + /// The requested font family name, or . + /// The culture used for matching, or . + /// The matched family name. + /// The matched style. + /// if a matching system font was found; otherwise, . + public static bool TryMatchCharacter( + CodePoint codePoint, + FontStyle style, + string? familyName, + CultureInfo? culture, + out string? matchedFamilyName, + out FontStyle matchedStyle) + { + if (OperatingSystem.IsWindows()) + { + return DirectWriteSystemFontMatcher.TryMatchCharacter( + codePoint, + style, + familyName, + culture, + out matchedFamilyName, + out matchedStyle); + } + + if (OperatingSystem.IsMacOS()) + { + return CoreTextSystemFontMatcher.TryMatchCharacter( + codePoint, + style, + familyName, + culture, + out matchedFamilyName, + out matchedStyle); + } + + if (OperatingSystem.IsLinux()) + { + return FontConfigSystemFontMatcher.TryMatchCharacter( + codePoint, + style, + familyName, + culture, + out matchedFamilyName, + out matchedStyle); + } + + matchedFamilyName = null; + matchedStyle = default; + return false; + } +} diff --git a/src/SixLabors.Fonts/PlaceholderGlyphMetrics.cs b/src/SixLabors.Fonts/PlaceholderGlyphMetrics.cs index edfa262d2..d2d68c166 100644 --- a/src/SixLabors.Fonts/PlaceholderGlyphMetrics.cs +++ b/src/SixLabors.Fonts/PlaceholderGlyphMetrics.cs @@ -65,8 +65,15 @@ internal override void RenderTo( int graphemeIndex, Vector2 glyphOrigin, Vector2 decorationOrigin, + Vector2 layoutAdvance, GlyphLayoutMode mode, - TextOptions options) + TextRun textRun, + float pointSize, + float dpi, + HintingMode hintingMode, + TextDecorationSkipInk textDecorationSkipInk, + DecorationPositioningMode decorationPositioningMode, + FontMetrics decorationFontMetrics) { // Placeholders reserve layout space only; the caller owns the object rendering. } diff --git a/src/SixLabors.Fonts/Rendering/EmboldeningGlyphRenderer.cs b/src/SixLabors.Fonts/Rendering/EmboldeningGlyphRenderer.cs index ec2549988..7a401918c 100644 --- a/src/SixLabors.Fonts/Rendering/EmboldeningGlyphRenderer.cs +++ b/src/SixLabors.Fonts/Rendering/EmboldeningGlyphRenderer.cs @@ -12,25 +12,29 @@ namespace SixLabors.Fonts.Rendering; /// used by web browsers. /// /// -/// Outline contours are buffered per fill group (a color layer, or the whole glyph when no -/// layers are present) so that the group's overall winding can be determined. Every point, -/// including off-curve control points, is then shifted along the local outline normal using a -/// mitred offset, which grows the filled area while shrinking any counters, just as a real bold -/// weight would. The dilated contours are replayed to the wrapped renderer, preserving the -/// original segment types. +/// Outline contours are buffered per fill group so their overall winding can be determined. +/// The points are then moved using the same bounded lateral-bisector algorithm as FreeType's +/// FT_Outline_EmboldenXY, and replayed with their original segment types. /// internal sealed class EmboldeningGlyphRenderer : IGlyphRenderer { - private readonly IGlyphRenderer inner; - private readonly float strength; - private readonly List group = new(); + private static readonly ObjectPool Pool = new(new PooledObjectPolicy()); + + private readonly List group = []; + private readonly List availableContours = []; + private IGlyphRenderer inner = null!; + private float strength; private Contour? current; + private EmboldeningGlyphRenderer() + { + } + /// /// Initializes a new instance of the class. /// /// The renderer that receives the dilated outline. - /// The outward offset applied to each outline edge, in pixels. + /// The total x/y outline strength, in pixels. public EmboldeningGlyphRenderer(IGlyphRenderer inner, float strength) { this.inner = inner; @@ -45,6 +49,25 @@ private enum SegmentType : byte Cubic, } + /// + /// Rents a renderer configured to forward the emboldened outline to the given renderer. + /// + /// The renderer that receives the dilated outline. + /// The total x/y outline strength, in pixels. + /// The configured renderer. + public static EmboldeningGlyphRenderer Rent(IGlyphRenderer inner, float strength) + { + EmboldeningGlyphRenderer renderer = Pool.Get(); + renderer.inner = inner; + renderer.strength = strength; + return renderer; + } + + /// + /// Returns this renderer and its retained contour buffers to the shared pool. + /// + public void Release() => Pool.Return(this); + /// public void BeginText(in FontRectangle bounds) => this.inner.BeginText(in bounds); @@ -74,7 +97,7 @@ public void EndLayer() } /// - public void BeginFigure() => this.current = new Contour(); + public void BeginFigure() => this.current = this.GetContour(); /// public void MoveTo(Vector2 point) => this.Add(SegmentType.Move, point); @@ -109,70 +132,138 @@ public void EndFigure() public TextDecorations EnabledDecorations() => this.inner.EnabledDecorations(); /// - public void SetDecoration(TextDecorations textDecorations, Vector2 start, Vector2 end, float thickness) - => this.inner.SetDecoration(textDecorations, start, end, thickness); + public void SetDecoration(TextDecorations textDecorations, Vector2 start, Vector2 end, float thickness, ReadOnlyMemory intersections) + => this.inner.SetDecoration(textDecorations, start, end, thickness, intersections); - private static Vector2[] Offset(List points, float strength) + /// + /// Emits any buffered contours after the owning glyph outline has been fully decoded. + /// + public void CompleteOutline() => this.Flush(); + + /// + /// Moves one contour's points using FreeType's bounded lateral-bisector dilation. + /// + /// The contour points, including off-curve control points. + /// The total x/y outline strength. + /// + /// when the complete outline uses FreeType's TrueType orientation; + /// otherwise for PostScript orientation. + /// + private static void Embolden(List points, float strength, bool trueTypeOrientation) { - int n = points.Count; - Vector2[] result = new Vector2[n]; - for (int i = 0; i < n; i++) + // FreeType treats the requested value as the total increase and moves points using half + // that amount in each axis. The remaining movement comes from the local edge bisector. + strength *= .5F; + + int first = 0; + int last = points.Count - 1; + int i = last; + int j = first; + int anchorIndex = -1; + Vector2 incoming = default; + Vector2 anchor = default; + float incomingLength = 0F; + float anchorLength = 0F; + + // This is a direct float translation of FT_Outline_EmboldenXY. j examines successive + // points, i advances only when points are moved, and anchorIndex closes the contour after + // coincident points have been skipped. + while (j != i && i != anchorIndex) { - Vector2 p = points[i]; - Vector2 e1 = Normalize(p - points[(i - 1 + n) % n]); - Vector2 e2 = Normalize(points[(i + 1) % n] - p); - - // Edge normals (rotate each edge direction by -90 degrees). - Vector2 n1 = new(e1.Y, -e1.X); - Vector2 n2 = new(e2.Y, -e2.X); - Vector2 sum = n1 + n2; - - Vector2 direction; - if (sum == Vector2.Zero) + Vector2 outgoing; + float outgoingLength; + if (j != anchorIndex) { - direction = n1 == Vector2.Zero ? n2 : n1; + outgoing = points[j] - points[i]; + outgoingLength = outgoing.Length(); + if (outgoingLength == 0F) + { + j = j < last ? j + 1 : first; + continue; + } + + outgoing /= outgoingLength; } else { - // Mitre so that each adjacent edge moves out by exactly the strength. - // The denominator is clamped to tame the mitre spike at sharp corners. - float d = 1F + Vector2.Dot(n1, n2); - direction = sum / MathF.Max(d, 0.25F); + outgoing = anchor; + outgoingLength = anchorLength; } - result[i] = p + (strength * direction); - } + if (incomingLength != 0F) + { + if (anchorIndex < 0) + { + anchorIndex = i; + anchor = incoming; + anchorLength = incomingLength; + } - return result; - } + float dot = Vector2.Dot(incoming, outgoing); + Vector2 shift = default; - private static Vector2 Normalize(Vector2 v) - { - float length = v.Length(); - return length < 1e-6F ? Vector2.Zero : v / length; - } + // FreeType leaves turns of roughly 160 degrees or more untouched. This avoids + // unstable bisectors where two edges almost reverse direction. + if (dot > -0.9375F) + { + float denominator = dot + 1F; + shift = new Vector2(incoming.Y + outgoing.Y, incoming.X + outgoing.X); + + if (trueTypeOrientation) + { + shift.X = -shift.X; + } + else + { + shift.Y = -shift.Y; + } + + float cross = (outgoing.X * incoming.Y) - (outgoing.Y * incoming.X); + if (trueTypeOrientation) + { + cross = -cross; + } + + // Limit each component by the shorter adjacent edge. FreeType uses this + // branch to stop collapsing segments from producing unbounded corner spikes. + float length = MathF.Min(incomingLength, outgoingLength); + shift.X = (strength * cross) <= (length * denominator) + ? shift.X * strength / denominator + : shift.X * length / cross; + + shift.Y = (strength * cross) <= (length * denominator) + ? shift.Y * strength / denominator + : shift.Y * length / cross; + } - private static float SignedArea(List points) - { - float area = 0F; - for (int i = 0, j = points.Count - 1; i < points.Count; j = i++) - { - area += (points[j].X * points[i].Y) - (points[i].X * points[j].Y); - } + Vector2 delta = new(strength + shift.X, strength + shift.Y); + while (i != j) + { + points[i] += delta; + i = i < last ? i + 1 : first; + } + } + else + { + i = j; + } - return area * 0.5F; + incoming = outgoing; + incomingLength = outgoingLength; + j = j < last ? j + 1 : first; + } } private void Add(SegmentType type, Vector2 p0) { - Contour contour = this.current ??= new Contour(); + Contour contour = this.current ??= this.GetContour(); contour.Segments.Add((type, 1)); contour.Points.Add(p0); } private void Add(SegmentType type, Vector2 p0, Vector2 p1) { - Contour contour = this.current ??= new Contour(); + Contour contour = this.current ??= this.GetContour(); contour.Segments.Add((type, 2)); contour.Points.Add(p0); contour.Points.Add(p1); @@ -180,13 +271,26 @@ private void Add(SegmentType type, Vector2 p0, Vector2 p1) private void Add(SegmentType type, Vector2 p0, Vector2 p1, Vector2 p2) { - Contour contour = this.current ??= new Contour(); + Contour contour = this.current ??= this.GetContour(); contour.Segments.Add((type, 3)); contour.Points.Add(p0); contour.Points.Add(p1); contour.Points.Add(p2); } + private Contour GetContour() + { + int index = this.availableContours.Count - 1; + if (index >= 0) + { + Contour contour = this.availableContours[index]; + this.availableContours.RemoveAt(index); + return contour; + } + + return new Contour(); + } + private void Flush() { if (this.group.Count == 0) @@ -194,22 +298,33 @@ private void Flush() return; } - // Grow the fill outward regardless of the source outline's winding convention: - // a positive total area means the group is wound counter-clockwise, for which the - // edge normals already point outward, otherwise the offset direction is reversed. - // Only on-curve anchor points are used so that extreme cubic control points cannot - // distort the winding calculation. - float area = 0F; + // FreeType determines one orientation for the complete outline using the non-zero winding + // rule and the polygon formed by every point, including off-curve control points. Using the + // group orientation makes outer contours grow while oppositely wound counters shrink. + double area = 0D; foreach (Contour contour in this.group) { - area += SignedArea(contour.Anchors()); + List points = contour.Points; + Vector2 previous = points[^1]; + for (int i = 0; i < points.Count; i++) + { + Vector2 current = points[i]; + area += ((double)current.Y - previous.Y) * (current.X + previous.X); + previous = current; + } } - float signedStrength = (area >= 0F ? 1F : -1F) * this.strength; + bool hasOrientation = area != 0D; + bool trueTypeOrientation = area < 0D; foreach (Contour contour in this.group) { - Vector2[] offset = Offset(contour.Points, signedStrength); + List points = contour.Points; + if (hasOrientation) + { + Embolden(points, this.strength, trueTypeOrientation); + } + this.inner.BeginFigure(); int index = 0; @@ -218,16 +333,16 @@ private void Flush() switch (type) { case SegmentType.Move: - this.inner.MoveTo(offset[index]); + this.inner.MoveTo(points[index]); break; case SegmentType.Line: - this.inner.LineTo(offset[index]); + this.inner.LineTo(points[index]); break; case SegmentType.Quadratic: - this.inner.QuadraticBezierTo(offset[index], offset[index + 1]); + this.inner.QuadraticBezierTo(points[index], points[index + 1]); break; case SegmentType.Cubic: - this.inner.CubicBezierTo(offset[index], offset[index + 1], offset[index + 2]); + this.inner.CubicBezierTo(points[index], points[index + 1], points[index + 2]); break; } @@ -237,31 +352,59 @@ private void Flush() this.inner.EndFigure(); } + this.RecycleGroup(); + } + + private void Reset() + { + if (this.current is not null) + { + this.current.Clear(); + this.availableContours.Add(this.current); + this.current = null; + } + + this.RecycleGroup(); + this.inner = null!; + this.strength = 0F; + } + + private void RecycleGroup() + { + // Keep each contour's backing arrays with the pooled renderer so subsequent glyphs + // only overwrite retained storage instead of allocating per contour. + foreach (Contour contour in this.group) + { + contour.Clear(); + this.availableContours.Add(contour); + } + this.group.Clear(); } private sealed class Contour { - public List Points { get; } = new(); + public List Points { get; } = []; - public List<(SegmentType Type, int Count)> Segments { get; } = new(); + public List<(SegmentType Type, int Count)> Segments { get; } = []; - /// - /// Gets the on-curve anchor points of the contour, i.e. the endpoint of each segment, - /// which describe the contour's winding independently of any off-curve control points. - /// - /// The anchor points. - public List Anchors() + public void Clear() { - List anchors = new(this.Segments.Count); - int index = 0; - foreach ((SegmentType _, int count) in this.Segments) - { - index += count; - anchors.Add(this.Points[index - 1]); - } + this.Points.Clear(); + this.Segments.Clear(); + } + } + + private sealed class PooledObjectPolicy : IPooledObjectPolicy + { + /// + public EmboldeningGlyphRenderer Create() => new(); - return anchors; + /// + public bool Return(EmboldeningGlyphRenderer obj) + { + obj.Reset(); + return true; } } } diff --git a/src/SixLabors.Fonts/Rendering/GlyphIntersectionCollector.cs b/src/SixLabors.Fonts/Rendering/GlyphIntersectionCollector.cs new file mode 100644 index 000000000..54eafeb72 --- /dev/null +++ b/src/SixLabors.Fonts/Rendering/GlyphIntersectionCollector.cs @@ -0,0 +1,358 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.Fonts.Rendering; + +/// +/// A glyph renderer that computes, per outline contour, the along-line interval spanned by the +/// contour's ink where it crosses a band, without rasterizing anything. This measures the ink that +/// a decoration line would cross so the line can be interrupted around it, as CSS +/// text-decoration-skip-ink requires. The band lies across the line axis: horizontal lines +/// band on y and collect x extents; vertical lines band on x and collect y extents. Each contour +/// contributes a single [min, max] interval bounding every place its outline reaches into +/// the band: the extent is grown by each crossing of the band's two edges and by every flattened +/// outline point that lies inside the band. A single bounding interval per contour tracks the ink +/// tightly and leaves no hole over it, so the decoration line can never be drawn across a stroke +/// that crosses it. The counter of a filled contour is bridged rather than threaded, keeping the +/// line from fragmenting: the spec leaves the exact gaps UA-defined and allows widening or dropping +/// ones that would be too small to be useful. Overlapping contour intervals are merged when the +/// span is built, and the consumer widens each by a clearance and re-merges, which also absorbs the +/// sub-pixel slack of the fixed curve flattening. Instances are reusable: rearms +/// the collector for a new band without allocating, so a render loop can share one collector across +/// every glyph it decorates. +/// +internal sealed class GlyphIntersectionCollector : IGlyphRenderer +{ + /// + /// The number of line segments a Bezier curve is subdivided into when locating its crossings + /// of the band edges and its points inside the band. Decoration skipping tolerates sub-pixel + /// error, which the consumer's clearance absorbs, so a fixed subdivision avoids the cost of + /// adaptive flattening. + /// + private const int CurveSegments = 24; + + private float bandStart; + private float bandEnd; + private bool vertical; + private readonly List<(float Start, float End)> intervals = []; + + /// + /// Reusable buffer holding the merged interval pairs produced by + /// ; grown on demand and kept across resets. + /// + private float[] merged = []; + + private Vector2 currentPoint; + private Vector2 figureStart; + + /// + /// Whether a figure has been started and not yet closed. Its bounding interval is finalized + /// when the figure closes, so an unterminated figure is closed implicitly. + /// + private bool figureOpen; + + /// + /// The along-line extent of the current figure's ink inside the band, grown as the outline is + /// decoded. Empty ( greater than ) until the + /// figure first reaches the band. + /// + private float figureLeft; + private float figureRight; + + /// + /// Initializes a new instance of the class with + /// an empty band. must be called before collecting. + /// + public GlyphIntersectionCollector() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// One edge of the band on the cross axis. + /// The other edge of the band on the cross axis. + /// + /// to band on x and collect y extents for vertical lines; + /// to band on y and collect x extents for horizontal lines. + /// + public GlyphIntersectionCollector(float lowerLimit, float upperLimit, bool vertical = false) + => this.Reset(lowerLimit, upperLimit, vertical); + + /// + /// Rearms the collector for a new band, discarding any previously collected state while + /// retaining the internal buffers so reuse does not allocate. + /// + /// One edge of the band on the cross axis. + /// The other edge of the band on the cross axis. + /// + /// to band on x and collect y extents for vertical lines; + /// to band on y and collect x extents for horizontal lines. + /// + public void Reset(float lowerLimit, float upperLimit, bool vertical) + { + this.bandStart = Math.Min(lowerLimit, upperLimit); + this.bandEnd = Math.Max(lowerLimit, upperLimit); + this.vertical = vertical; + this.intervals.Clear(); + this.figureOpen = false; + this.figureLeft = float.MaxValue; + this.figureRight = float.MinValue; + } + + /// + public void BeginText(in FontRectangle bounds) + { + } + + /// + public void EndText() + { + } + + /// + public bool BeginGlyph(in FontRectangle bounds, in GlyphRendererParameters parameters) + + // Outlines are only worth decoding when the glyph's box can reach the band. + => this.vertical + ? bounds.Right >= this.bandStart && bounds.Left <= this.bandEnd + : bounds.Bottom >= this.bandStart && bounds.Top <= this.bandEnd; + + /// + public void EndGlyph() => this.CloseOpenFigure(); + + /// + public void BeginLayer(Paint? paint, FillRule fillRule, ClipQuad? clipBounds) + { + } + + /// + public void EndLayer() + { + } + + /// + public void BeginFigure() + { + } + + /// + public void MoveTo(Vector2 point) + { + this.CloseOpenFigure(); + this.currentPoint = point; + this.figureStart = point; + this.figureOpen = true; + this.figureLeft = float.MaxValue; + this.figureRight = float.MinValue; + } + + /// + public void LineTo(Vector2 point) + { + this.AddSegment(this.currentPoint, point); + this.currentPoint = point; + } + + /// + public void QuadraticBezierTo(Vector2 secondControlPoint, Vector2 point) + { + Vector2 previous = this.currentPoint; + Vector2 last = previous; + for (int i = 1; i <= CurveSegments; i++) + { + float t = i / (float)CurveSegments; + float u = 1F - t; + Vector2 sample = (u * u * previous) + (2F * u * t * secondControlPoint) + (t * t * point); + this.AddSegment(last, sample); + last = sample; + } + + this.currentPoint = point; + } + + /// + public void CubicBezierTo(Vector2 secondControlPoint, Vector2 thirdControlPoint, Vector2 point) + { + Vector2 previous = this.currentPoint; + Vector2 last = previous; + for (int i = 1; i <= CurveSegments; i++) + { + float t = i / (float)CurveSegments; + float u = 1F - t; + Vector2 sample = (u * u * u * previous) + + (3F * u * u * t * secondControlPoint) + + (3F * u * t * t * thirdControlPoint) + + (t * t * t * point); + this.AddSegment(last, sample); + last = sample; + } + + this.currentPoint = point; + } + + /// + public void ArcTo(float radiusX, float radiusY, float rotation, bool largeArc, bool sweep, Vector2 point) + { + // Font outlines are emitted as lines and Bezier curves; arcs do not occur in practice. + // Fall back to the chord so an unexpected arc still contributes conservative coverage. + this.AddSegment(this.currentPoint, point); + this.currentPoint = point; + } + + /// + public void EndFigure() => this.CloseOpenFigure(); + + /// + public TextDecorations EnabledDecorations() => TextDecorations.None; + + /// + public void SetDecoration(TextDecorations textDecorations, Vector2 start, Vector2 end, float thickness, ReadOnlyMemory intersections) + { + } + + /// + /// Builds the merged, sorted flattened interval pairs collected so far. + /// + /// The flattened interval pairs, or an empty array when nothing crossed the band. + public float[] BuildIntersections() + => this.BuildIntersectionSpan().ToArray(); + + /// + /// Builds the merged, sorted flattened interval pairs collected so far into a reusable + /// internal buffer. The span is valid until the next build or ; callers + /// that need the values to escape must copy them. + /// + /// The flattened interval pairs, or an empty span when nothing crossed the band. + public ReadOnlyMemory BuildIntersectionSpan() + { + this.CloseOpenFigure(); + + if (this.intervals.Count == 0) + { + return ReadOnlyMemory.Empty; + } + + this.intervals.Sort(static (a, b) => a.Start.CompareTo(b.Start)); + + int required = this.intervals.Count * 2; + if (this.merged.Length < required) + { + this.merged = new float[Math.Max(required, this.merged.Length * 2)]; + } + + int count = 0; + (float start, float end) = this.intervals[0]; + for (int i = 1; i < this.intervals.Count; i++) + { + (float Start, float End) interval = this.intervals[i]; + if (interval.Start <= end) + { + end = Math.Max(end, interval.End); + continue; + } + + this.merged[count++] = start; + this.merged[count++] = end; + (start, end) = interval; + } + + this.merged[count++] = start; + this.merged[count++] = end; + return new ReadOnlyMemory(this.merged, 0, count); + } + + /// + /// Grows the current figure's along-line extent by one outline segment: by each point at which + /// the segment crosses either band edge, and by either endpoint that lies inside the band. + /// + /// The segment start point. + /// The segment end point. + private void AddSegment(Vector2 from, Vector2 to) + { + float fromBand = this.vertical ? from.X : from.Y; + float toBand = this.vertical ? to.X : to.Y; + float fromLine = this.vertical ? from.Y : from.X; + float toLine = this.vertical ? to.Y : to.X; + + this.ExpandCrossing(this.bandStart, fromBand, toBand, fromLine, toLine); + this.ExpandCrossing(this.bandEnd, fromBand, toBand, fromLine, toLine); + + if (fromBand > this.bandStart && fromBand < this.bandEnd) + { + this.Expand(fromLine); + } + + if (toBand > this.bandStart && toBand < this.bandEnd) + { + this.Expand(toLine); + } + } + + /// + /// Grows the current figure's along-line extent by the point at which a segment crosses one + /// band edge, if it crosses it at all. + /// + /// The edge's cross-axis coordinate. + /// The segment start on the cross axis. + /// The segment end on the cross axis. + /// The segment start on the line axis. + /// The segment end on the line axis. + private void ExpandCrossing(float edge, float fromBand, float toBand, float fromLine, float toLine) + { + bool fromBelow = fromBand <= edge; + bool toBelow = toBand <= edge; + if (fromBelow == toBelow) + { + return; + } + + float t = (edge - fromBand) / (toBand - fromBand); + this.Expand(fromLine + ((toLine - fromLine) * t)); + } + + /// + /// Grows the current figure's along-line extent to include one coordinate. + /// + /// The along-line coordinate to include. + private void Expand(float line) + { + if (line < this.figureLeft) + { + this.figureLeft = line; + } + + if (line > this.figureRight) + { + this.figureRight = line; + } + } + + /// + /// Closes the open figure, if any, by intersecting its implicit closing edge with the band and + /// then committing its bounding extent as one interval when the figure reached the band. + /// + private void CloseOpenFigure() + { + if (this.figureOpen) + { + this.figureOpen = false; + if (this.currentPoint != this.figureStart) + { + this.AddSegment(this.currentPoint, this.figureStart); + } + + this.currentPoint = this.figureStart; + + if (this.figureLeft < this.figureRight) + { + this.intervals.Add((this.figureLeft, this.figureRight)); + } + + this.figureLeft = float.MaxValue; + this.figureRight = float.MinValue; + } + } +} diff --git a/src/SixLabors.Fonts/Rendering/GlyphRendererExtensions.cs b/src/SixLabors.Fonts/Rendering/GlyphRendererExtensions.cs index 510bb8390..c6b81eed8 100644 --- a/src/SixLabors.Fonts/Rendering/GlyphRendererExtensions.cs +++ b/src/SixLabors.Fonts/Rendering/GlyphRendererExtensions.cs @@ -17,7 +17,7 @@ public static class GlyphRendererExtensions /// Returns the original public static IGlyphRenderer Render(this IGlyphRenderer renderer, ReadOnlySpan text, TextOptions options) { - new TextRenderer(renderer).RenderText(text, options); + new TextRenderer(renderer).Render(text, options); return renderer; } } diff --git a/src/SixLabors.Fonts/Rendering/IGlyphRenderer.cs b/src/SixLabors.Fonts/Rendering/IGlyphRenderer.cs index d24b48c01..41b998862 100644 --- a/src/SixLabors.Fonts/Rendering/IGlyphRenderer.cs +++ b/src/SixLabors.Fonts/Rendering/IGlyphRenderer.cs @@ -131,5 +131,11 @@ public interface IGlyphRenderer /// 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); + /// + /// The along-line intervals, in device pixels, where the glyph's ink crosses the decoration band, + /// given as sorted [start, end] pairs. Empty when skip-ink does not apply. The full, + /// untrimmed line is reported; a renderer that honours skip-ink carves the line around these + /// intervals, widened by the thickness, and may extend a gap into an adjacent glyph. + /// + public void SetDecoration(TextDecorations textDecorations, Vector2 start, Vector2 end, float thickness, ReadOnlyMemory intersections); } diff --git a/src/SixLabors.Fonts/Rendering/PaintedGlyph.cs b/src/SixLabors.Fonts/Rendering/PaintedGlyph.cs index 30222b685..b30cc55a8 100644 --- a/src/SixLabors.Fonts/Rendering/PaintedGlyph.cs +++ b/src/SixLabors.Fonts/Rendering/PaintedGlyph.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Numerics; + namespace SixLabors.Fonts.Rendering; /// @@ -12,13 +14,22 @@ internal readonly struct PaintedGlyph /// Initializes a new instance of the struct. /// /// The painted layers. - public PaintedGlyph(List layers) => this.Layers = layers; + public PaintedGlyph(List layers) + { + this.Layers = layers; + this.Bounds = CalculateBounds(layers); + } /// /// Gets the layers for this glyph. /// public IReadOnlyList Layers { get; } + /// + /// Gets the cached bounds of all painted geometry after applying each layer transform. + /// + public Bounds Bounds { get; } + /// /// Gets a value indicating whether this glyph has no layers. /// @@ -28,4 +39,90 @@ internal readonly struct PaintedGlyph /// Gets an empty glyph instance. /// public static PaintedGlyph Empty => new([]); + + private static Bounds CalculateBounds(List layers) + { + Vector2 min = new(float.MaxValue); + Vector2 max = new(float.MinValue); + bool hasPoint = false; + + for (int i = 0; i < layers.Count; i++) + { + PaintedLayer layer = layers[i]; + IReadOnlyList path = layer.Path; + Vector2 current = default; + + for (int j = 0; j < path.Count; j++) + { + PathCommand command = path[j]; + switch (command.Verb) + { + case PathVerb.MoveTo: + case PathVerb.LineTo: + Include(command.EndPoint, layer.Transform, ref min, ref max, ref hasPoint); + break; + case PathVerb.QuadraticTo: + Include(command.ControlPoint1, layer.Transform, ref min, ref max, ref hasPoint); + Include(command.EndPoint, layer.Transform, ref min, ref max, ref hasPoint); + break; + case PathVerb.CubicTo: + Include(command.ControlPoint1, layer.Transform, ref min, ref max, ref hasPoint); + Include(command.ControlPoint2, layer.Transform, ref min, ref max, ref hasPoint); + Include(command.EndPoint, layer.Transform, ref min, ref max, ref hasPoint); + break; + case PathVerb.ArcTo: + IncludeArc(current, command, layer.Transform, ref min, ref max, ref hasPoint); + break; + } + + if (command.Verb != PathVerb.ClosePath) + { + current = command.EndPoint; + } + } + } + + return hasPoint ? new Bounds(min, max) : Bounds.Empty; + } + + private static void Include( + Vector2 point, + Matrix3x2 transform, + ref Vector2 min, + ref Vector2 max, + ref bool hasPoint) + { + point = Vector2.Transform(point, transform); + min = Vector2.Min(min, point); + max = Vector2.Max(max, point); + hasPoint = true; + } + + private static void IncludeArc( + Vector2 start, + PathCommand command, + Matrix3x2 transform, + ref Vector2 min, + ref Vector2 max, + ref bool hasPoint) + { + Include(start, transform, ref min, ref max, ref hasPoint); + Include(command.EndPoint, transform, ref min, ref max, ref hasPoint); + + float radiusX = MathF.Abs(command.RadiusX); + float radiusY = MathF.Abs(command.RadiusY); + float angle = command.RotationDegrees * (MathF.PI / 180F); + float cos = MathF.Cos(angle); + float sin = MathF.Sin(angle); + float extentX = MathF.Sqrt((radiusX * radiusX * cos * cos) + (radiusY * radiusY * sin * sin)); + float extentY = MathF.Sqrt((radiusX * radiusX * sin * sin) + (radiusY * radiusY * cos * cos)); + + // The endpoint lies on the ellipse, so twice its axis extents around that point enclose + // the complete ellipse and therefore the selected arc without resolving its center. + Vector2 extent = new(extentX * 2F, extentY * 2F); + Bounds arcBounds = new(command.EndPoint - extent, command.EndPoint + extent); + arcBounds = Bounds.Transform(in arcBounds, transform); + min = Vector2.Min(min, arcBounds.Min); + max = Vector2.Max(max, arcBounds.Max); + } } diff --git a/src/SixLabors.Fonts/Rendering/PaintedGlyphMetrics.cs b/src/SixLabors.Fonts/Rendering/PaintedGlyphMetrics.cs index 15acfaec4..4d1d55b80 100644 --- a/src/SixLabors.Fonts/Rendering/PaintedGlyphMetrics.cs +++ b/src/SixLabors.Fonts/Rendering/PaintedGlyphMetrics.cs @@ -109,64 +109,57 @@ internal override FontGlyphMetrics CloneForRendering(TextRun textRun) textRun); /// - internal override void RenderTo( + internal override Bounds GetDesignBounds() + { + if (!this.source.TryGetPaintedGlyph(this.GlyphId, out PaintedGlyph glyph, out PaintedCanvasMetadata canvas)) + { + return base.GetDesignBounds(); + } + + Matrix3x2 sourceToUpem = ComputeSourceToUpem(canvas, this.UnitsPerEm); + return Bounds.Transform(glyph.Bounds, sourceToUpem); + } + + /// + internal override void RenderOutlineTo( IGlyphRenderer renderer, - int graphemeIndex, Vector2 glyphOrigin, - Vector2 decorationOrigin, GlyphLayoutMode mode, - TextOptions options) + float scaledPPEM, + HintingMode hintingMode) { - if (ShouldSkipGlyphRendering(this.CodePoint)) + if (!this.source.TryGetPaintedGlyph(this.GlyphId, out PaintedGlyph glyph, out PaintedCanvasMetadata canvas)) { return; } - float pointSize = this.TextRun.Font?.Size ?? options.Font.Size; - float dpi = options.Dpi; - - // Device-space placement. - glyphOrigin *= dpi; - decorationOrigin *= dpi; + Vector2 scale = new Vector2(scaledPPEM) / this.ScaleFactor; // uniform + Matrix3x2 outlineTransform = this.GetOutlineTransform(mode); - 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. + // Keep painted geometry in Y-up font space through the same scale, offset, oblique, and + // rotation sequence used by TrueType and CFF, then perform the device-space Y inversion. Matrix3x2 layout = Matrix3x2.CreateScale(scale); - layout *= rotation; - layout.Translation = (this.Offset * scale) + glyphOrigin; + layout.Translation = this.Offset * scale; + layout *= outlineTransform; + layout *= Matrix3x2.CreateScale(1F, -1F); + layout.Translation += glyphOrigin; - // Bounds in device space for BeginGlyph. - FontRectangle box = this.GetBoundingBox(mode, glyphOrigin, scaledPpem); - GlyphRendererParameters parameters = new(this, this.TextRun, pointSize, dpi, mode, graphemeIndex); + FontRectangle box = this.GetBoundingBox(mode, glyphOrigin, scaledPPEM); - 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); + // 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; + // 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, decorationOrigin, mode, rotation, scaledPpem, options); - } + // Stream layers and commands with correct transforms. + StreamPaintedGlyph(glyph, in box, renderer, total); } /// /// 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. + /// Enforces a uniform 'meet' scale from the root viewBox (if present) and normalizes + /// Y-down document coordinates to the Y-up glyph-metrics coordinate system. /// private static Matrix3x2 ComputeSourceToUpem(in PaintedCanvasMetadata canvas, ushort upem) { @@ -192,10 +185,10 @@ private static Matrix3x2 ComputeSourceToUpem(in PaintedCanvasMetadata canvas, us m = m * t * sUni; } - // Coordinate system orientation. - if (!canvas.IsYDown) + // Normalize every painted source to the Y-up font-space contract used by glyph metrics. + if (canvas.IsYDown) { - // Flip Y around the origin; placement happens in layout. + // SVG document coordinates are Y-down; COLR outlines are already Y-up. m *= Matrix3x2.CreateScale(1f, -1f); } diff --git a/src/SixLabors.Fonts/Rendering/TeeGlyphRenderer.cs b/src/SixLabors.Fonts/Rendering/TeeGlyphRenderer.cs new file mode 100644 index 000000000..828221977 --- /dev/null +++ b/src/SixLabors.Fonts/Rendering/TeeGlyphRenderer.cs @@ -0,0 +1,145 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.Fonts.Rendering; + +/// +/// A glyph renderer that forwards every callback to a primary renderer while mirroring outline +/// geometry into up to two instances. This lets +/// decoration skip-ink measure a glyph's ink from the same emission the renderer receives, +/// including hinting, without decoding the outline a second time. Instances are reusable: +/// rearms the tee for a new glyph without allocating, and +/// drops the target references once the glyph completes so a pooled tee +/// does not root the renderer. +/// +internal sealed class TeeGlyphRenderer : IGlyphRenderer +{ + /// + /// The renderer that receives every callback unchanged. Never null between + /// and , the only window in which callbacks occur. + /// + private IGlyphRenderer primary = null!; + + /// + /// The collector observing the underline band, if any. + /// + private GlyphIntersectionCollector? underlineInk; + + /// + /// The collector observing the overline band, if any. + /// + private GlyphIntersectionCollector? overlineInk; + + /// + /// Rearms the tee to forward to the given renderer and mirror geometry into the given + /// collectors. + /// + /// The renderer that receives every callback unchanged. + /// The collector observing the underline band, if any. + /// The collector observing the overline band, if any. + public void Reset( + IGlyphRenderer primary, + GlyphIntersectionCollector? underlineInk, + GlyphIntersectionCollector? overlineInk) + { + this.primary = primary; + this.underlineInk = underlineInk; + this.overlineInk = overlineInk; + } + + /// + /// Drops the forwarding targets so a pooled tee does not keep the renderer or collectors + /// reachable between glyphs. + /// + public void Clear() + { + this.primary = null!; + this.underlineInk = null; + this.overlineInk = null; + } + + /// + public void BeginText(in FontRectangle bounds) => this.primary.BeginText(in bounds); + + /// + public void EndText() => this.primary.EndText(); + + /// + public bool BeginGlyph(in FontRectangle bounds, in GlyphRendererParameters parameters) + => this.primary.BeginGlyph(in bounds, in parameters); + + /// + public void EndGlyph() => this.primary.EndGlyph(); + + /// + public void BeginLayer(Paint? paint, FillRule fillRule, ClipQuad? clipBounds) + => this.primary.BeginLayer(paint, fillRule, clipBounds); + + /// + public void EndLayer() => this.primary.EndLayer(); + + /// + public void BeginFigure() + { + this.primary.BeginFigure(); + this.underlineInk?.BeginFigure(); + this.overlineInk?.BeginFigure(); + } + + /// + public void EndFigure() + { + this.primary.EndFigure(); + this.underlineInk?.EndFigure(); + this.overlineInk?.EndFigure(); + } + + /// + public void MoveTo(Vector2 point) + { + this.primary.MoveTo(point); + this.underlineInk?.MoveTo(point); + this.overlineInk?.MoveTo(point); + } + + /// + public void LineTo(Vector2 point) + { + this.primary.LineTo(point); + this.underlineInk?.LineTo(point); + this.overlineInk?.LineTo(point); + } + + /// + public void QuadraticBezierTo(Vector2 secondControlPoint, Vector2 point) + { + this.primary.QuadraticBezierTo(secondControlPoint, point); + this.underlineInk?.QuadraticBezierTo(secondControlPoint, point); + this.overlineInk?.QuadraticBezierTo(secondControlPoint, point); + } + + /// + public void CubicBezierTo(Vector2 secondControlPoint, Vector2 thirdControlPoint, Vector2 point) + { + this.primary.CubicBezierTo(secondControlPoint, thirdControlPoint, point); + this.underlineInk?.CubicBezierTo(secondControlPoint, thirdControlPoint, point); + this.overlineInk?.CubicBezierTo(secondControlPoint, thirdControlPoint, point); + } + + /// + public void ArcTo(float radiusX, float radiusY, float rotation, bool largeArc, bool sweep, Vector2 point) + { + this.primary.ArcTo(radiusX, radiusY, rotation, largeArc, sweep, point); + this.underlineInk?.ArcTo(radiusX, radiusY, rotation, largeArc, sweep, point); + this.overlineInk?.ArcTo(radiusX, radiusY, rotation, largeArc, sweep, point); + } + + /// + public TextDecorations EnabledDecorations() => this.primary.EnabledDecorations(); + + /// + public void SetDecoration(TextDecorations textDecorations, Vector2 start, Vector2 end, float thickness, ReadOnlyMemory intersections) + => this.primary.SetDecoration(textDecorations, start, end, thickness, intersections); +} diff --git a/src/SixLabors.Fonts/Rendering/TextRenderer.cs b/src/SixLabors.Fonts/Rendering/TextRenderer.cs index 01ff5a853..f50879fc1 100644 --- a/src/SixLabors.Fonts/Rendering/TextRenderer.cs +++ b/src/SixLabors.Fonts/Rendering/TextRenderer.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Numerics; + namespace SixLabors.Fonts.Rendering; /// @@ -8,6 +10,13 @@ namespace SixLabors.Fonts.Rendering; /// public class TextRenderer { + /// + /// Indicates that no laid-out advance is available for a glyph: rendering starts from a + /// glyph id rather than laid-out text, so decorations derive their length from the glyph's + /// own metric advance. + /// + private static readonly Vector2 NoLayoutAdvance = new(-1F); + private readonly IGlyphRenderer renderer; /// @@ -22,8 +31,8 @@ public class TextRenderer /// The target renderer. /// The text to render. /// The text options. controls wrapping; use -1 to disable wrapping. - public static void RenderTextTo(IGlyphRenderer renderer, ReadOnlySpan text, TextOptions options) - => new TextRenderer(renderer).RenderText(text, options); + public static void RenderTo(IGlyphRenderer renderer, ReadOnlySpan text, TextOptions options) + => new TextRenderer(renderer).Render(text, options); /// /// Renders the text to the . @@ -31,25 +40,115 @@ public static void RenderTextTo(IGlyphRenderer renderer, ReadOnlySpan text /// The target renderer. /// The text to render. /// The text options. controls wrapping; use -1 to disable wrapping. - public static void RenderTextTo(IGlyphRenderer renderer, string text, TextOptions options) - => new TextRenderer(renderer).RenderText(text, options); + public static void RenderTo(IGlyphRenderer renderer, string text, TextOptions options) + => new TextRenderer(renderer).Render(text, options); + + /// + /// Renders a glyph id to the . + /// + /// The target renderer. + /// The glyph identifier. + /// The glyph options. + public static void RenderTo(IGlyphRenderer renderer, ushort glyphId, GlyphOptions options) + => new TextRenderer(renderer).Render(glyphId, options); + + /// + /// Renders glyph ids to the . + /// + /// The target renderer. + /// The glyph run. + /// The glyph options. + public static void RenderTo(IGlyphRenderer renderer, GlyphRun glyphRun, GlyphOptions options) + => new TextRenderer(renderer).Render(glyphRun, options); /// /// Renders the text to the configured renderer. /// /// The text to render. /// The text options. controls wrapping; use -1 to disable wrapping. - public void RenderText(string text, TextOptions options) - => this.RenderText(text.AsSpan(), options); + public void Render(string text, TextOptions options) + => this.Render(text.AsSpan(), options); /// /// Renders the text to the configured renderer. /// /// The text to render. /// The text options. controls wrapping; use -1 to disable wrapping. - public void RenderText(ReadOnlySpan text, TextOptions options) + public void Render(ReadOnlySpan text, TextOptions options) { TextBlock block = new(text, options); block.RenderTo(this.renderer, options.WrappingLength); } + + /// + /// Renders a glyph id to the configured renderer. + /// + /// The glyph identifier. + /// The glyph options. + public void Render(ushort glyphId, GlyphOptions options) + { + FontMetrics fontMetrics = options.Font.FontMetrics; + if (!fontMetrics.TryGetGlyphMetrics( + glyphId, + options.TextAttributes, + options.TextDecorations, + options.LayoutMode, + options.ColorFontSupport, + out FontGlyphMetrics? metrics)) + { + return; + } + + TextRun textRun = options.CreateTextRun(); + FontGlyphMetrics renderMetrics = metrics.CloneForRendering(textRun); + Vector2 origin = options.Origin / options.Dpi; + GlyphLayoutMode glyphLayoutMode = options.GetGlyphLayoutMode(metrics.CodePoint); + + renderMetrics.RenderTo( + this.renderer, + options.GraphemeIndex, + origin, + origin, + NoLayoutAdvance, + glyphLayoutMode, + textRun, + options.Font.Size, + options.Dpi, + options.HintingMode, + options.TextDecorationSkipInk, + options.DecorationPositioningMode, + fontMetrics); + } + + /// + /// Renders glyph ids to the configured renderer. + /// + /// The glyph run. + /// The glyph options. + public void Render(GlyphRun glyphRun, GlyphOptions options) + { + Guard.NotNull(glyphRun, nameof(glyphRun)); + Guard.NotNull(options, nameof(options)); + + ReadOnlySpan glyphIds = glyphRun.GlyphIds.Span; + ReadOnlySpan origins = glyphRun.Origins.Span; + Vector2 originalOrigin = options.Origin; + int originalGraphemeIndex = options.GraphemeIndex; + + try + { + for (int i = 0; i < glyphIds.Length; i++) + { + options.Origin = origins[i]; + options.GraphemeIndex = originalGraphemeIndex + i; + + this.Render(glyphIds[i], options); + } + } + finally + { + options.Origin = originalOrigin; + options.GraphemeIndex = originalGraphemeIndex; + } + } } diff --git a/src/SixLabors.Fonts/StreamFontMetrics.Cff.cs b/src/SixLabors.Fonts/StreamFontMetrics.Cff.cs index 7c0f0bf5b..61a71c45f 100644 --- a/src/SixLabors.Fonts/StreamFontMetrics.Cff.cs +++ b/src/SixLabors.Fonts/StreamFontMetrics.Cff.cs @@ -21,7 +21,7 @@ namespace SixLabors.Fonts; /// internal partial class StreamFontMetrics { - private static StreamFontMetrics LoadCompactFont(FontReader reader) + private static StreamFontMetrics LoadCompactFont(FontReader reader, FontSource source) { // Load using recommended order for best performance. // https://learn.microsoft.com/en-gb/typography/opentype/spec/recom#optimized-table-ordering @@ -94,7 +94,7 @@ private static StreamFontMetrics LoadCompactFont(FontReader reader) Svg = svg }; - return new StreamFontMetrics(tables, glyphVariationProcessor); + return new StreamFontMetrics(tables, source, glyphVariationProcessor); } private FontGlyphMetrics CreateCffGlyphMetrics( diff --git a/src/SixLabors.Fonts/StreamFontMetrics.TrueType.cs b/src/SixLabors.Fonts/StreamFontMetrics.TrueType.cs index b29ad1907..a5ec9e737 100644 --- a/src/SixLabors.Fonts/StreamFontMetrics.TrueType.cs +++ b/src/SixLabors.Fonts/StreamFontMetrics.TrueType.cs @@ -96,7 +96,7 @@ internal void ApplyTrueTypeHinting(HintingMode hintingMode, FontGlyphMetrics met } } - private static StreamFontMetrics LoadTrueTypeFont(FontReader reader) + private static StreamFontMetrics LoadTrueTypeFont(FontReader reader, FontSource source) { // Load using recommended order for best performance. // https://learn.microsoft.com/en-gb/typography/opentype/spec/recom#optimized-table-ordering @@ -178,7 +178,7 @@ private static StreamFontMetrics LoadTrueTypeFont(FontReader reader) glyphVariationProcessor = new GlyphVariationProcessor(itemVariationStore, fvar, avar, gvar, hvar, vvar, mvar, cvar); } - return new StreamFontMetrics(tables, glyphVariationProcessor); + return new StreamFontMetrics(tables, source, glyphVariationProcessor); } private FontGlyphMetrics CreateTrueTypeGlyphMetrics( diff --git a/src/SixLabors.Fonts/StreamFontMetrics.cs b/src/SixLabors.Fonts/StreamFontMetrics.cs index 29dc9d05f..35dd5b554 100644 --- a/src/SixLabors.Fonts/StreamFontMetrics.cs +++ b/src/SixLabors.Fonts/StreamFontMetrics.cs @@ -35,6 +35,7 @@ internal partial class StreamFontMetrics : FontMetrics private readonly ConcurrentDictionary<(int CodePoint, ushort Id, TextAttributes Attributes, ColorFontSupport ColorSupport, bool IsVerticalLayout), FontGlyphMetrics> glyphCache; private readonly ConcurrentDictionary<(int CodePoint, int NextCodePoint), (bool Success, ushort GlyphId, bool SkipNextCodePoint)> glyphIdCache; private readonly ConcurrentDictionary codePointCache; + private readonly FontSource source; private SvgGlyphSource? svgGlyphSource; private readonly FontDescription description; private readonly HorizontalMetrics horizontalMetrics; @@ -59,8 +60,9 @@ internal partial class StreamFontMetrics : FontMetrics /// Initializes a new instance of the class. /// /// The True Type font tables. + /// The source used to retrieve table data for this face. /// An optional glyph variation processor for handling variable fonts. - internal StreamFontMetrics(TrueTypeFontTables tables, GlyphVariationProcessor? glyphVariationProcessor = null) + public StreamFontMetrics(TrueTypeFontTables tables, FontSource source, GlyphVariationProcessor? glyphVariationProcessor = null) { this.trueTypeFontTables = tables; this.outlineType = OutlineType.TrueType; @@ -69,6 +71,7 @@ internal StreamFontMetrics(TrueTypeFontTables tables, GlyphVariationProcessor? g this.glyphIdCache = new(); this.codePointCache = new(); this.glyphCache = new(); + this.source = source; (HorizontalMetrics HorizontalMetrics, VerticalMetrics VerticalMetrics) metrics = this.Initialize(tables); this.horizontalMetrics = metrics.HorizontalMetrics; @@ -81,8 +84,9 @@ internal StreamFontMetrics(TrueTypeFontTables tables, GlyphVariationProcessor? g /// Initializes a new instance of the class. /// /// The Compact Font tables. + /// The source used to retrieve table data for this face. /// An optional glyph variation processor for handling variable fonts. - internal StreamFontMetrics(CompactFontTables tables, GlyphVariationProcessor? glyphVariationProcessor = null) + public StreamFontMetrics(CompactFontTables tables, FontSource source, GlyphVariationProcessor? glyphVariationProcessor = null) { this.compactFontTables = tables; this.outlineType = OutlineType.CFF; @@ -91,6 +95,7 @@ internal StreamFontMetrics(CompactFontTables tables, GlyphVariationProcessor? gl this.glyphIdCache = new(); this.codePointCache = new(); this.glyphCache = new(); + this.source = source; (HorizontalMetrics HorizontalMetrics, VerticalMetrics VerticalMetrics) metrics = this.Initialize(tables); this.horizontalMetrics = metrics.HorizontalMetrics; @@ -107,7 +112,8 @@ private StreamFontMetrics( GlyphVariationProcessor processor, ConcurrentDictionary<(int CodePoint, int NextCodePoint), (bool Success, ushort GlyphId, bool SkipNextCodePoint)> sharedGlyphIdCache, ConcurrentDictionary sharedCodePointCache, - SvgGlyphSource? svgGlyphSource) + SvgGlyphSource? svgGlyphSource, + FontSource source) { this.trueTypeFontTables = tables; this.outlineType = OutlineType.TrueType; @@ -117,6 +123,7 @@ private StreamFontMetrics( this.codePointCache = sharedCodePointCache; this.glyphCache = new(); this.svgGlyphSource = svgGlyphSource; + this.source = source; (HorizontalMetrics HorizontalMetrics, VerticalMetrics VerticalMetrics) metrics = this.Initialize(tables); this.horizontalMetrics = metrics.HorizontalMetrics; @@ -135,7 +142,8 @@ private StreamFontMetrics( GlyphVariationProcessor processor, ConcurrentDictionary<(int CodePoint, int NextCodePoint), (bool Success, ushort GlyphId, bool SkipNextCodePoint)> sharedGlyphIdCache, ConcurrentDictionary sharedCodePointCache, - SvgGlyphSource? svgGlyphSource) + SvgGlyphSource? svgGlyphSource, + FontSource source) { this.compactFontTables = tables; this.outlineType = OutlineType.CFF; @@ -145,6 +153,7 @@ private StreamFontMetrics( this.codePointCache = sharedCodePointCache; this.glyphCache = new(); this.svgGlyphSource = svgGlyphSource; + this.source = source; (HorizontalMetrics HorizontalMetrics, VerticalMetrics VerticalMetrics) metrics = this.Initialize(tables); this.horizontalMetrics = metrics.HorizontalMetrics; @@ -209,6 +218,14 @@ private StreamFontMetrics( /// public override float ItalicAngle => this.italicAngle; + /// + public override bool TryGetTableData(Tag tag, out ReadOnlyMemory table) + => this.source.TryGetTableData(tag, out table); + + /// + public override Stream OpenStream() + => this.source.OpenStream(); + /// internal override bool TryGetGlyphId(CodePoint codePoint, out ushort glyphId) => this.TryGetGlyphId(codePoint, null, out glyphId, out bool _); @@ -329,7 +346,35 @@ public override bool TryGetGlyphMetrics( // 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 != null; + return true; + } + + /// + public override bool TryGetGlyphMetrics( + ushort glyphId, + TextAttributes textAttributes, + TextDecorations textDecorations, + LayoutMode layoutMode, + ColorFontSupport support, + [NotNullWhen(true)] out FontGlyphMetrics? metrics) + { + ushort glyphCount = this.outlineType == OutlineType.TrueType + ? this.trueTypeFontTables!.Maxp.GlyphCount + : this.compactFontTables!.Maxp.GlyphCount; + + if (glyphId >= glyphCount) + { + metrics = null; + return false; + } + + // The outline is keyed by glyph id, not codepoint, so a glyph renders regardless of whether the + // reverse cmap lookup succeeds. Ligatures, substituted glyphs and .notdef have no codepoint, and + // bailing here would drop them entirely. Recover the codepoint when possible (it only feeds the + // skip/whitespace heuristics and GlyphRendererParameters.CodePoint); otherwise fall back to default. + _ = this.TryGetCodePoint(glyphId, out CodePoint codePoint); + metrics = this.GetGlyphMetrics(codePoint, glyphId, textAttributes, textDecorations, layoutMode, support); + return true; } /// @@ -343,7 +388,12 @@ internal override FontGlyphMetrics GetGlyphMetrics( // We overwrite the cache entry for this type should the attributes change. => this.glyphCache.GetOrAdd( - CreateCacheKey(in codePoint, glyphId, textAttributes, support, layoutMode), + CreateCacheKey( + in codePoint, + glyphId, + textAttributes, + support, + AdvancedTypographicUtils.IsVerticalGlyph(codePoint, layoutMode)), static (key, arg) => arg.Item3.CreateGlyphMetrics( @@ -477,7 +527,7 @@ internal override ReadOnlySpan GetNormalizedCoordinates() /// /// The variation axis settings to apply. /// A new configured for the requested variation. - internal StreamFontMetrics CreateVariationInstance(FontVariation[] variations) + public StreamFontMetrics CreateVariationInstance(FontVariation[] variations) { FVarTable? fvar = this.outlineType == OutlineType.TrueType ? this.trueTypeFontTables?.Fvar @@ -502,7 +552,7 @@ internal StreamFontMetrics CreateVariationInstance(FontVariation[] variations) FontVariation variation = variations[v]; for (int i = 0; i < fvar.AxisCount; i++) { - if (string.Equals(fvar.Axes[i].Tag, variation.Tag, StringComparison.Ordinal)) + if (fvar.Axes[i].Tag == variation.Tag) { userCoordinates[i] = variation.Value; break; @@ -526,7 +576,7 @@ internal StreamFontMetrics CreateVariationInstance(FontVariation[] variations) tables.Cvar, userCoordinates); - return new StreamFontMetrics(tables, processor, this.glyphIdCache, this.codePointCache, this.svgGlyphSource); + return new StreamFontMetrics(tables, processor, this.glyphIdCache, this.codePointCache, this.svgGlyphSource, this.source); } else { @@ -542,54 +592,30 @@ internal StreamFontMetrics CreateVariationInstance(FontVariation[] variations) tables.MVar, userCoordinates: userCoordinates); - return new StreamFontMetrics(tables, processor, this.glyphIdCache, this.codePointCache, this.svgGlyphSource); + return new StreamFontMetrics(tables, processor, this.glyphIdCache, this.codePointCache, this.svgGlyphSource, this.source); } } /// /// Reads a from the specified stream. /// - /// The file path. - /// a . - public static StreamFontMetrics LoadFont(string path) - { - using FileStream fs = File.OpenRead(path); - using FontReader reader = new(fs); - return LoadFont(reader); - } - - /// - /// Reads a from the specified stream. - /// - /// The file path. - /// Position in the stream to read the font from. - /// a . - public static StreamFontMetrics LoadFont(string path, long offset) - { - using FileStream fs = File.OpenRead(path); - fs.Position = offset; - return LoadFont(fs); - } - - /// - /// Reads a from the specified stream. - /// - /// The stream. - /// a . - public static StreamFontMetrics LoadFont(Stream stream) + /// The stream positioned at the font face data. + /// The source used to retrieve raw font data for this face. + /// A . + public static StreamFontMetrics LoadFont(Stream stream, FontSource source) { using FontReader reader = new(stream); - return LoadFont(reader); + return LoadFont(reader, source); } - internal static StreamFontMetrics LoadFont(FontReader reader) + private static StreamFontMetrics LoadFont(FontReader reader, FontSource source) { if (reader.OutlineType == OutlineType.TrueType) { - return LoadTrueTypeFont(reader); + return LoadTrueTypeFont(reader, source); } - return LoadCompactFont(reader); + return LoadCompactFont(reader, source); } private (HorizontalMetrics HorizontalMetrics, VerticalMetrics VerticalMetrics) Initialize(T tables) @@ -777,45 +803,13 @@ private void ApplyMVarDeltas(HorizontalMetrics horizontalMetrics, VerticalMetric this.underlineThickness += (short)MathF.Round(processor.GetMVarDelta(MVarTag.UnderlineThickness)); } - /// - /// Reads a from the specified stream. - /// - /// The file path. - /// A read-only memory region containing the font metrics. - public static ReadOnlyMemory LoadFontCollection(string path) - { - using FileStream fs = File.OpenRead(path); - return LoadFontCollection(fs); - } - - /// - /// Reads a from the specified stream. - /// - /// The stream. - /// A read-only memory region containing the font metrics. - public static ReadOnlyMemory LoadFontCollection(Stream stream) - { - long startPos = stream.Position; - 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) - { - stream.Position = startPos + ttcHeader.OffsetTable[i]; - fonts[i] = LoadFont(stream); - } - - return fonts; - } - 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, colorSupport, AdvancedTypographicUtils.IsVerticalGlyph(codePoint, layoutMode)); + bool isVerticalLayout) + => (codePoint.Value, glyphId, textAttributes, colorSupport, isVerticalLayout); private FontGlyphMetrics CreateGlyphMetrics( in CodePoint codePoint, diff --git a/src/SixLabors.Fonts/SystemFontCollection.cs b/src/SixLabors.Fonts/SystemFontCollection.cs index 65bb26fd1..3bbadb2eb 100644 --- a/src/SixLabors.Fonts/SystemFontCollection.cs +++ b/src/SixLabors.Fonts/SystemFontCollection.cs @@ -4,6 +4,8 @@ using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Runtime.InteropServices; +using SixLabors.Fonts.Unicode; +using SixLabors.Fonts.WellKnownIds; namespace SixLabors.Fonts; @@ -12,8 +14,22 @@ namespace SixLabors.Fonts; /// internal sealed class SystemFontCollection : IReadOnlySystemFontCollection, IReadOnlyFontMetricsCollection { - private readonly FontCollection collection; - private readonly IReadOnlyCollection searchDirectories; + private readonly Lazy familyNames; + private readonly Lazy familyFaces; + private readonly Lazy fontPaths; + private readonly Lazy searchDirectories; + private readonly Dictionary familyMetrics = new(StringComparer.OrdinalIgnoreCase); + private readonly object familyMetricsLock = new(); + + /// + /// Name IDs that identify the family grouping exposed by font platforms. + /// + private static readonly KnownNameIds[] FamilyNameIds = + [ + KnownNameIds.FontFamilyName, + KnownNameIds.TypographicFamilyName, + KnownNameIds.WwsFamilyName, + ]; /// /// Gets the default set of locations we probe for System Fonts. @@ -68,95 +84,272 @@ static SystemFontCollection() public SystemFontCollection() { - IEnumerable paths; - Native.MacSystemFontsEnumerator? nativeEnumerator = null; - - bool forceDirectoryEnumeration = AppContext.TryGetSwitch("Switch.SixLabors.Fonts.DoNotUseNativeSystemFontsEnumeration", out bool isEnabled) && isEnabled; - if (!forceDirectoryEnumeration && RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - { - nativeEnumerator = new Native.MacSystemFontsEnumerator(); - - // The CTFontManagerCopyAvailableFontURLs method might return duplicate paths, hence the call to Distinct() - paths = nativeEnumerator.Distinct(); + this.familyNames = new Lazy(this.GetSystemFamilyNames, true); + this.familyFaces = new Lazy(this.GetSystemFamilyFaces, true); + this.fontPaths = new Lazy(this.GetFontPaths, true); + this.searchDirectories = new Lazy(this.GetSearchDirectories, true); + } - this.searchDirectories = Array.Empty(); - } - else + /// + public IEnumerable Families + { + get { - string[] expanded = [.. StandardFontLocations.Select(Environment.ExpandEnvironmentVariables)]; - string[] existingDirectories = [.. expanded.Where(x => Directory.Exists(x))]; + string[] names = this.familyNames.Value; + FontFamily[] families = new FontFamily[names.Length]; - // We do this to provide a consistent experience with case sensitive file systems. - paths = existingDirectories - .SelectMany(x => Directory.EnumerateFiles(x, "*.*", SearchOption.AllDirectories)) - .Where(x => Path.GetExtension(x).Equals(".ttf", StringComparison.OrdinalIgnoreCase) - || Path.GetExtension(x).Equals(".ttc", StringComparison.OrdinalIgnoreCase) - || Path.GetExtension(x).Equals(".otf", StringComparison.OrdinalIgnoreCase)); + for (int i = 0; i < names.Length; i++) + { + families[i] = new FontFamily(names[i], this, CultureInfo.InvariantCulture); + } - this.searchDirectories = existingDirectories; + return families; } - - this.collection = CreateSystemFontCollection(paths, this.searchDirectories); - - nativeEnumerator?.Dispose(); } /// - public IEnumerable Families => this.collection.Families; - - /// - public IEnumerable SearchDirectories => this.searchDirectories; + public IEnumerable SearchDirectories => this.searchDirectories.Value; /// public FontFamily Get(string name) => this.GetByCulture(name, CultureInfo.InvariantCulture); /// public bool TryGet(string name, out FontFamily family) - => this.collection.TryGet(name, out family); + => this.TryGetByCulture(name, CultureInfo.InvariantCulture, out family); /// public IEnumerable GetByCulture(CultureInfo culture) - => this.collection.GetByCulture(culture); + { + string[] names = this.familyNames.Value; + FontFamily[] families = new FontFamily[names.Length]; + + for (int i = 0; i < names.Length; i++) + { + families[i] = new FontFamily(names[i], this, culture); + } + + return families; + } /// public FontFamily GetByCulture(string name, CultureInfo culture) - => this.collection.GetByCulture(name, culture); + { + if (this.TryGetByCulture(name, culture, out FontFamily family)) + { + return family; + } + + throw new FontFamilyNotFoundException(name, this.searchDirectories.Value); + } /// public bool TryGetByCulture(string name, CultureInfo culture, out FontFamily family) - => this.collection.TryGetByCulture(name, culture, out family); + { + Guard.NotNull(name, nameof(name)); + + if (!this.TryGetKnownFamilyName(name, culture, out string familyName)) + { + family = default; + return false; + } + + SystemFontFamilyMetrics[] metrics = this.GetOrLoadFamilyMetrics(familyName, culture); + if (metrics.Length == 0) + { + family = default; + return false; + } + + family = new FontFamily(familyName, this, culture); + return true; + } + + /// + public bool TryMatchCharacter( + CodePoint codePoint, + FontStyle style, + string? familyName, + CultureInfo? culture, + out FontMatch match) + { + if (Native.SystemFontMatcher.TryMatchCharacter(codePoint, style, familyName, culture, out string? matchedFamilyName, out FontStyle matchedStyle) + && matchedFamilyName is not null + && this.TryGetMatchedFamily(matchedFamilyName, culture, out FontFamily family)) + { + if (!family.TryGetMetrics(matchedStyle, out _)) + { + matchedStyle = family.TryGetMetrics(style, out _) + ? style + : family.GetAvailableStyles().Span[0]; + } + + match = new FontMatch(family, matchedStyle); + return true; + } + + match = default; + return false; + } /// bool IReadOnlyFontMetricsCollection.TryGetMetrics(string name, CultureInfo culture, FontStyle style, [NotNullWhen(true)] out FontMetrics? metrics) - => ((IReadOnlyFontMetricsCollection)this.collection).TryGetMetrics(name, culture, style, out metrics); + { + SystemFontFamilyMetrics[] familyMetrics = this.GetOrLoadFamilyMetrics(name, culture); + + foreach (SystemFontFamilyMetrics fontMetrics in familyMetrics) + { + if (fontMetrics.Style == style) + { + metrics = fontMetrics.Metrics; + return true; + } + } + + metrics = null; + return false; + } /// IEnumerable IReadOnlyFontMetricsCollection.GetAllMetrics(string name, CultureInfo culture) - => ((IReadOnlyFontMetricsCollection)this.collection).GetAllMetrics(name, culture); + => this.GetOrLoadFamilyMetrics(name, culture).Select(x => x.Metrics).ToArray(); /// ReadOnlyMemory IReadOnlyFontMetricsCollection.GetAllStyles(string name, CultureInfo culture) - => ((IReadOnlyFontMetricsCollection)this.collection).GetAllStyles(name, culture); + { + SystemFontFamilyMetrics[] familyMetrics = this.GetOrLoadFamilyMetrics(name, culture); + FontStyle[] styles = new FontStyle[familyMetrics.Length]; + + for (int i = 0; i < familyMetrics.Length; i++) + { + styles[i] = familyMetrics[i].Style; + } + + return styles; + } /// IEnumerator IReadOnlyFontMetricsCollection.GetEnumerator() - => ((IReadOnlyFontMetricsCollection)this.collection).GetEnumerator(); + { + foreach (string familyName in this.familyNames.Value) + { + foreach (SystemFontFamilyMetrics metrics in this.GetOrLoadFamilyMetrics(familyName, CultureInfo.InvariantCulture)) + { + yield return metrics.Metrics; + } + } + } - private static FontCollection CreateSystemFontCollection(IEnumerable paths, IReadOnlyCollection searchDirectories) + /// + /// Gets all system font metrics with their platform family names. + /// + /// The system font family metrics. + internal IEnumerable GetAllFamilyMetrics() { - FontCollection collection = new(searchDirectories); + foreach (string familyName in this.familyNames.Value) + { + foreach (SystemFontFamilyMetrics metrics in this.GetOrLoadFamilyMetrics(familyName, CultureInfo.InvariantCulture)) + { + yield return metrics; + } + } + } + + /// + bool IReadOnlyFontMetricsCollection.TryGetMetrics( + string name, + CultureInfo culture, + FontStyle style, + FontWeight weight, + [NotNullWhen(true)] out FontMetrics? metrics) + { + bool italic = (style & FontStyle.Italic) == FontStyle.Italic; + + return TryFindWeight(this.GetOrLoadFamilyMetrics(name, culture), italic, weight, out metrics); + } - foreach (string path in paths) + /// + /// Attempts to resolve a native fallback family name using the requested culture before falling back to invariant lookup. + /// + /// The family name returned by the native matcher. + /// The requested culture, or to use invariant lookup. + /// The matched family. + /// when the family is known and its metrics can be loaded. + private bool TryGetMatchedFamily(string familyName, CultureInfo? culture, out FontFamily family) + { + if (culture is not null && this.TryGetByCulture(familyName, culture, out family)) + { + return true; + } + + if (this.TryGet(familyName, out family)) + { + return true; + } + + return this.TryGetByCulture(familyName, CultureInfo.InvariantCulture, out family); + } + + /// + /// Gets the metrics for a single family, loading them from disk the first time that family is requested. + /// + /// The family name. + /// The culture used to compare localized family names. + /// The metrics belonging to the requested family. + private SystemFontFamilyMetrics[] GetOrLoadFamilyMetrics(string name, CultureInfo culture) + { + string key = string.Concat(culture.Name, '\0', name); + + lock (this.familyMetricsLock) + { + if (this.familyMetrics.TryGetValue(key, out SystemFontFamilyMetrics[]? metrics)) + { + return metrics; + } + + metrics = this.LoadFamilyMetrics(name, culture); + this.familyMetrics.Add(key, metrics); + return metrics; + } + } + + /// + /// Loads the metrics for one family from the system font files. + /// + /// The family name. + /// The culture used to compare localized family names. + /// The metrics belonging to the requested family. + private SystemFontFamilyMetrics[] LoadFamilyMetrics(string name, CultureInfo culture) + { + Native.NativeSystemFontFace[] nativeFaces = this.familyFaces.Value; + if (nativeFaces.Length > 0) + { + return LoadNativeFamilyMetrics(name, culture, nativeFaces); + } + + List metrics = []; + StringComparer comparer = StringComparerHelpers.GetCaseInsensitiveStringComparer(culture); + + foreach (string path in this.fontPaths.Value) { try { - if (path.EndsWith(".ttc", StringComparison.OrdinalIgnoreCase)) + if (IsFontCollectionPath(path)) { - _ = collection.AddCollection(path); + foreach (FileFontMetrics fontMetrics in FileFontMetrics.LoadFontCollection(path).Span) + { + if (MatchesFamilyName(fontMetrics.Description, name, culture, comparer)) + { + metrics.Add(new SystemFontFamilyMetrics(name, fontMetrics.Description.Style, fontMetrics)); + } + } } else { - _ = collection.Add(path); + FileFontMetrics fontMetrics = new(path); + if (MatchesFamilyName(fontMetrics.Description, name, culture, comparer)) + { + metrics.Add(new SystemFontFamilyMetrics(name, fontMetrics.Description.Style, fontMetrics)); + } } } catch @@ -165,6 +358,370 @@ private static FontCollection CreateSystemFontCollection(IEnumerable pat } } - return collection; + return [.. metrics]; + } + + /// + /// Loads metrics for one native system family from resolved platform font faces. + /// + /// The family name. + /// The culture used to compare localized family names. + /// The resolved native font faces. + /// The metrics belonging to the requested family. + private static SystemFontFamilyMetrics[] LoadNativeFamilyMetrics(string name, CultureInfo culture, Native.NativeSystemFontFace[] nativeFaces) + { + List familyFaces = []; + StringComparer comparer = StringComparerHelpers.GetCaseInsensitiveStringComparer(culture); + + foreach (Native.NativeSystemFontFace face in nativeFaces) + { + if (comparer.Equals(face.FamilyName, name)) + { + familyFaces.Add(face); + } + } + + // Style-only lookup returns the first face in its FontStyle bucket. Keep every numeric + // weight in the family, but place the platform's closest 400/700 face first so adding + // numeric lookup does not change the established regular, bold, italic, or bold-italic face. + familyFaces.Sort(static (left, right) => + { + int styleComparison = left.Style.CompareTo(right.Style); + if (styleComparison != 0) + { + return styleComparison; + } + + return left.StyleScore.CompareTo(right.StyleScore); + }); + + List metrics = []; + foreach (Native.NativeSystemFontFace face in familyFaces) + { + AddNativeFamilyMetrics(metrics, face); + } + + return [.. metrics]; + } + + /// + /// Finds the browser-preferred face for an OpenType weight and slant. + /// + /// The OS-enumerated faces in the family. + /// Whether an italic or oblique face is required. + /// The requested OpenType weight. + /// The matching metrics. + /// when a face with the requested slant is available. + private static bool TryFindWeight( + SystemFontFamilyMetrics[] familyMetrics, + bool italic, + FontWeight weight, + [NotNullWhen(true)] out FontMetrics? metrics) + { + int requestedWeight = (int)weight; + int bestDistance = int.MaxValue; + int bestWeight = int.MinValue; + FontMetrics? bestMatch = null; + + foreach (SystemFontFamilyMetrics candidate in familyMetrics) + { + bool candidateItalic = (candidate.Style & FontStyle.Italic) == FontStyle.Italic; + if (candidateItalic != italic) + { + continue; + } + + int candidateWeight = (int)candidate.Metrics.Description.Weight; + int distance = Math.Abs(candidateWeight - requestedWeight); + + if (distance == 0) + { + metrics = candidate.Metrics; + return true; + } + + // Windows system-family selection asks DirectWrite for the face that best matches the + // requested numeric weight: + // https://learn.microsoft.com/windows/win32/api/dwrite/nf-dwrite-idwritefontfamily-getfirstmatchingfont + // Select the nearest installed weight and prefer the heavier face when two weights are + // equally distant. The tie rule reproduces Windows browser selection for Segoe UI 500, + // which resolves to Semibold 600 rather than Regular 400. + if (distance < bestDistance || (distance == bestDistance && candidateWeight > bestWeight)) + { + bestDistance = distance; + bestWeight = candidateWeight; + bestMatch = candidate.Metrics; + } + } + + metrics = bestMatch; + return metrics is not null; + } + + /// + /// Adds metrics for a resolved native face. + /// + /// The metrics collection to update. + /// The native face to load. + private static void AddNativeFamilyMetrics(List metrics, Native.NativeSystemFontFace face) + { + try + { + if (TryLoadFontFace(face.Path, face.FaceIndex, out FileFontMetrics? fontMetrics)) + { + metrics.Add(new SystemFontFamilyMetrics(face.FamilyName, face.Style, fontMetrics)); + } + } + catch + { + // We swallow exceptions installing system fonts as we hold no guarantees about permissions etc. + } + } + + /// + /// Attempts to resolve a requested name against the native family-name list. + /// + /// The requested family name. + /// The culture used to compare localized family names. + /// The known family name to use for metric loading. + /// when the name is known, or when no native family-name list is available. + private bool TryGetKnownFamilyName(string name, CultureInfo culture, out string familyName) + { + string[] names = this.familyNames.Value; + if (names.Length == 0) + { + familyName = name; + return true; + } + + StringComparer comparer = StringComparerHelpers.GetCaseInsensitiveStringComparer(culture); + + foreach (string knownFamilyName in names) + { + if (comparer.Equals(knownFamilyName, name)) + { + familyName = knownFamilyName; + return true; + } + } + + familyName = string.Empty; + return false; + } + + /// + /// Gets the installed family names from the native matcher, falling back to font-file probing when native names are unavailable. + /// + /// The installed system font family names. + private string[] GetSystemFamilyNames() + { + Native.NativeSystemFontFace[] nativeFaces = this.familyFaces.Value; + if (nativeFaces.Length > 0) + { + HashSet names = new(StringComparer.OrdinalIgnoreCase); + + foreach (Native.NativeSystemFontFace face in nativeFaces) + { + _ = names.Add(face.FamilyName); + } + + return [.. names]; + } + + return this.GetFamilyNamesFromFontPaths(); + } + + /// + /// Gets the installed system font faces from the native matcher. + /// + /// The resolved native font faces, or an empty array when native face mapping is unavailable. + private Native.NativeSystemFontFace[] GetSystemFamilyFaces() + => Native.SystemFontMatcher.TryGetFamilyFaces(false, out Native.NativeSystemFontFace[]? faces) + ? faces + : []; + + /// + /// Loads family names by probing font descriptions from the system font files. + /// + /// The installed system font family names. + private string[] GetFamilyNamesFromFontPaths() + { + HashSet names = new(StringComparer.OrdinalIgnoreCase); + + foreach (string path in this.fontPaths.Value) + { + try + { + if (IsFontCollectionPath(path)) + { + foreach (FileFontMetrics fontMetrics in FileFontMetrics.LoadFontCollection(path).Span) + { + AddFamilyNames(names, fontMetrics.Description); + } + } + else + { + FileFontMetrics fontMetrics = new(path); + AddFamilyNames(names, fontMetrics.Description); + } + } + catch + { + // We swallow exceptions installing system fonts as we hold no guarantees about permissions etc. + } + } + + return [.. names]; + } + + /// + /// Adds every family-grouping name exposed by the font description. + /// + /// The family names. + /// The font description. + private static void AddFamilyNames(HashSet names, FontDescription description) + { + foreach (KnownNameIds nameId in FamilyNameIds) + { + string familyName = description.GetNameById(CultureInfo.InvariantCulture, nameId); + if (familyName.Length > 0) + { + _ = names.Add(familyName); + } + } + } + + /// + /// Returns whether the description exposes the requested family name through any family-grouping name ID. + /// + /// The font description. + /// The requested family name. + /// The culture used to compare localized family names. + /// The string comparer. + /// when the description contains the requested family name. + private static bool MatchesFamilyName(FontDescription description, string name, CultureInfo culture, StringComparer comparer) + { + foreach (KnownNameIds nameId in FamilyNameIds) + { + if (comparer.Equals(description.GetNameById(culture, nameId), name)) + { + return true; + } + } + + return false; + } + + /// + /// Gets the system font file paths used for lazy family metric loading. + /// + /// The installed system font file paths. + private string[] GetFontPaths() + { + Native.NativeSystemFontFace[] nativeFaces = this.familyFaces.Value; + if (nativeFaces.Length > 0) + { + HashSet paths = []; + + foreach (Native.NativeSystemFontFace face in nativeFaces) + { + _ = paths.Add(face.Path); + } + + return [.. paths]; + } + + // We do this to provide a consistent experience with case sensitive file systems. + return [.. this.searchDirectories.Value + .SelectMany(x => Directory.EnumerateFiles(x, "*.*", SearchOption.AllDirectories)) + .Where(IsSupportedFontFile)]; + } + + /// + /// Gets the system font directories used for directory-based probing. + /// + /// The existing system font directories. + private string[] GetSearchDirectories() + { + Native.NativeSystemFontFace[] nativeFaces = this.familyFaces.Value; + if (nativeFaces.Length > 0) + { + HashSet directories = new(StringComparer.OrdinalIgnoreCase); + + foreach (Native.NativeSystemFontFace face in nativeFaces) + { + string? directory = Path.GetDirectoryName(face.Path); + if (directory is { Length: > 0 }) + { + _ = directories.Add(directory); + } + } + + return [.. directories]; + } + + return GetStandardSearchDirectories(); + } + + /// + /// Gets the existing fallback directories used for font-file probing. + /// + /// The existing fallback directories. + private static string[] GetStandardSearchDirectories() + { + string[] expanded = [.. StandardFontLocations.Select(Environment.ExpandEnvironmentVariables)]; + return [.. expanded.Where(x => Directory.Exists(x))]; + } + + /// + /// Returns whether the path uses a supported system font file extension. + /// + /// The path to inspect. + /// when the file extension is supported. + private static bool IsSupportedFontFile(string path) + { + string extension = Path.GetExtension(path); + return extension.Equals(".ttf", StringComparison.OrdinalIgnoreCase) + || extension.Equals(".ttc", StringComparison.OrdinalIgnoreCase) + || extension.Equals(".otc", StringComparison.OrdinalIgnoreCase) + || extension.Equals(".otf", StringComparison.OrdinalIgnoreCase); + } + + /// + /// Gets whether a path identifies an OpenType font collection. + /// + /// The font file path. + /// for TTC and OTC files; otherwise, . + private static bool IsFontCollectionPath(string path) + { + string extension = Path.GetExtension(path); + return extension.Equals(".ttc", StringComparison.OrdinalIgnoreCase) + || extension.Equals(".otc", StringComparison.OrdinalIgnoreCase); + } + + /// + /// Tries to load a font face by file path and collection face index. + /// + /// The font file path. + /// The zero-based face index within the font file. + /// The font metrics. + /// if the font face was loaded; otherwise, . + private static bool TryLoadFontFace(string path, int faceIndex, [NotNullWhen(true)] out FileFontMetrics? metrics) + { + if (faceIndex == 0 && !IsFontCollectionPath(path)) + { + metrics = new FileFontMetrics(path); + return true; + } + + ReadOnlyMemory collection = FileFontMetrics.LoadFontCollection(path); + if ((uint)faceIndex < (uint)collection.Length) + { + metrics = collection.Span[faceIndex]; + return true; + } + + metrics = null; + return false; } } diff --git a/src/SixLabors.Fonts/SystemFontFamilyMetrics.cs b/src/SixLabors.Fonts/SystemFontFamilyMetrics.cs new file mode 100644 index 000000000..baca67890 --- /dev/null +++ b/src/SixLabors.Fonts/SystemFontFamilyMetrics.cs @@ -0,0 +1,38 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts; + +/// +/// Represents system font metrics associated with a platform family name. +/// +internal readonly struct SystemFontFamilyMetrics +{ + /// + /// Initializes a new instance of the struct. + /// + /// The platform family name. + /// The platform font style. + /// The font metrics. + public SystemFontFamilyMetrics(string familyName, FontStyle style, FontMetrics metrics) + { + this.FamilyName = familyName; + this.Style = style; + this.Metrics = metrics; + } + + /// + /// Gets the platform family name. + /// + public string FamilyName { get; } + + /// + /// Gets the platform font style. + /// + public FontStyle Style { get; } + + /// + /// Gets the font metrics. + /// + public FontMetrics Metrics { get; } +} diff --git a/src/SixLabors.Fonts/SystemFonts.cs b/src/SixLabors.Fonts/SystemFonts.cs index cb1b84a5d..0f4c8e0b8 100644 --- a/src/SixLabors.Fonts/SystemFonts.cs +++ b/src/SixLabors.Fonts/SystemFonts.cs @@ -1,7 +1,9 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Diagnostics.CodeAnalysis; using System.Globalization; +using SixLabors.Fonts.Unicode; namespace SixLabors.Fonts; @@ -22,6 +24,63 @@ public static class SystemFonts /// public static IEnumerable Families => Collection.Families; + /// + /// Gets the names of font families installed on current system. + /// + /// Whether the operating system should check for updates to the system font collection. + /// The installed system font family names. + public static string[] GetFamilyNames(bool checkForUpdates = false) + { + if (checkForUpdates && Native.SystemFontMatcher.TryGetFamilyFaces(checkForUpdates, out Native.NativeSystemFontFace[]? faces)) + { + HashSet names = new(StringComparer.OrdinalIgnoreCase); + + foreach (Native.NativeSystemFontFace face in faces) + { + names.Add(face.FamilyName); + } + + return [.. names]; + } + + List collectionNames = []; + + foreach (FontFamily family in Families) + { + collectionNames.Add(family.Name); + } + + return [.. collectionNames]; + } + + /// + /// Gets the operating system default font family name. + /// + /// The default font family name. + /// No system font families are available. + public static string GetDefaultFamilyName() + { + if (TryGetDefaultFamilyName(out string? familyName)) + { + return familyName; + } + + foreach (FontFamily family in Families) + { + return family.Name; + } + + throw new InvalidOperationException("No system font families are available."); + } + + /// + /// Tries to get the operating system default font family name. + /// + /// The operating system default font family name. + /// when the operating system returned a default font family name. + public static bool TryGetDefaultFamilyName([NotNullWhen(true)] out string? familyName) + => Native.SystemFontMatcher.TryGetDefaultFamilyName(out familyName); + /// public static FontFamily Get(string name) => GetByCulture(name, CultureInfo.InvariantCulture); @@ -60,6 +119,10 @@ public static FontFamily GetByCulture(string fontFamily, CultureInfo culture) public static bool TryGetByCulture(string fontFamily, CultureInfo culture, out FontFamily family) => Collection.TryGetByCulture(fontFamily, culture, out family); + /// + public static bool TryMatchCharacter(CodePoint codePoint, FontStyle style, string? familyName, CultureInfo? culture, out FontMatch match) + => Collection.TryMatchCharacter(codePoint, style, familyName, culture, out match); + /// /// Create a new instance of the for the named font family with regular styling. /// diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/VariationAxis.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/VariationAxis.cs index 44aa8a576..d926a379c 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/VariationAxis.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/VariationAxis.cs @@ -19,7 +19,7 @@ public readonly struct VariationAxis /// /// Gets tag identifying the design variation for the axis. /// - public string Tag { get; init; } + public Tag Tag { get; init; } /// /// Gets the minimum coordinate value for the axis. diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/VariationAxisRecord.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/VariationAxisRecord.cs index 2032c98f5..ac3b13555 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/VariationAxisRecord.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Variations/VariationAxisRecord.cs @@ -22,7 +22,7 @@ internal class VariationAxisRecord /// The maximum coordinate value for this axis. /// The axis qualifier flags. /// The name ID for the display name of this axis. - internal VariationAxisRecord(string tag, float minValue, float defaultValue, float maxValue, ushort flags, ushort axisNameId) + internal VariationAxisRecord(Tag tag, float minValue, float defaultValue, float maxValue, ushort flags, ushort axisNameId) { this.Tag = tag; this.MinValue = minValue; @@ -35,7 +35,7 @@ internal VariationAxisRecord(string tag, float minValue, float defaultValue, flo /// /// Gets the tag identifying the design variation for this axis (e.g. "wght", "wdth"). /// - public string Tag { get; } + public Tag Tag { get; } /// /// Gets the minimum coordinate value for this axis. @@ -89,7 +89,7 @@ public static VariationAxisRecord Load(BigEndianBinaryReader reader, long offset // +-----------------+----------------------------------------+----------------------------------------------------------------+ reader.Seek(offset, SeekOrigin.Begin); - string tag = reader.ReadTag(); + Tag tag = AdvancedTypographic.Tag.Parse(reader.ReadTag()); float minValue = reader.ReadFixed(); float defaultValue = reader.ReadFixed(); float maxValue = reader.ReadFixed(); diff --git a/src/SixLabors.Fonts/Tables/Cff/CffBoundsFinder.cs b/src/SixLabors.Fonts/Tables/Cff/CffBoundsFinder.cs index 6eaa1fe77..8f40569ec 100644 --- a/src/SixLabors.Fonts/Tables/Cff/CffBoundsFinder.cs +++ b/src/SixLabors.Fonts/Tables/Cff/CffBoundsFinder.cs @@ -163,7 +163,7 @@ 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, ReadOnlyMemory intersections) { // Do nothing. } diff --git a/src/SixLabors.Fonts/Tables/Cff/CffGlyphMetrics.cs b/src/SixLabors.Fonts/Tables/Cff/CffGlyphMetrics.cs index 7850fbf0c..97ac2631d 100644 --- a/src/SixLabors.Fonts/Tables/Cff/CffGlyphMetrics.cs +++ b/src/SixLabors.Fonts/Tables/Cff/CffGlyphMetrics.cs @@ -126,63 +126,46 @@ internal override FontGlyphMetrics CloneForRendering(TextRun textRun) this.GlyphType); /// - internal override void RenderTo( + internal override void RenderOutlineTo( IGlyphRenderer renderer, - int graphemeIndex, Vector2 glyphOrigin, - Vector2 decorationOrigin, GlyphLayoutMode mode, - TextOptions options) + float scaledPPEM, + HintingMode hintingMode) { - // https://www.unicode.org/faq/unsup_char.html - if (ShouldSkipGlyphRendering(this.CodePoint)) - { - return; - } - - float pointSize = this.TextRun.Font?.Size ?? options.Font.Size; - float dpi = options.Dpi; - - glyphOrigin *= dpi; - decorationOrigin *= dpi; - float scaledPPEM = this.GetScaledSize(pointSize, dpi); + Matrix3x2 transform = this.GetOutlineTransform(mode); - Matrix3x2 rotation = GetRotationMatrix(mode); + Vector2 scale = new Vector2(scaledPPEM) / this.ScaleFactor; - // Synthesize an oblique (faux italic) slant when requested but unavailable by shearing - // the outline in the glyph's Y-up space before rotation is applied. - float skew = this.GetObliqueSkew(); - Matrix3x2 transform = skew != 0F ? CreateObliqueMatrix(skew) * rotation : rotation; - - FontRectangle box = this.GetBoundingBox(mode, glyphOrigin, scaledPPEM); - GlyphRendererParameters parameters = new(this, this.TextRun, pointSize, dpi, mode, graphemeIndex); + // Apply the CFF FontMatrix to convert charstring coordinates to design units. + // The normalized FontMatrix (fontMatrix * unitsPerEM) is identity for the default + // [0.001, 0, 0, 0.001, 0, 0] with upm=1000. + if (this.glyphData.FontMatrix is double[] fm) + { + float upm = this.UnitsPerEm; + scale *= new Vector2((float)(fm[0] * upm), (float)(fm[3] * upm)); + } - // Synthesize a bold (faux bold) weight when requested but unavailable by dilating the - // outline through a decorator. Decorations continue to use the original renderer. + Vector2 scaledOffset = this.Offset * scale; float boldStrength = this.GetSyntheticBoldStrength(scaledPPEM); - IGlyphRenderer target = boldStrength > 0F ? new EmboldeningGlyphRenderer(renderer, boldStrength) : renderer; - - if (target.BeginGlyph(in box, in parameters)) + if (boldStrength > 0F) { - if (!UnicodeUtility.ShouldRenderWhiteSpaceOnly(this.CodePoint)) + // Flush through the supplied renderer before glyph completion so skip-ink observes + // the same synthesized outline as the drawing renderer. + EmboldeningGlyphRenderer target = EmboldeningGlyphRenderer.Rent(renderer, boldStrength); + try { - Vector2 scale = new Vector2(scaledPPEM) / this.ScaleFactor; - - // Apply the CFF FontMatrix to convert charstring coordinates to design units. - // The normalized FontMatrix (fontMatrix * unitsPerEM) is identity for the default - // [0.001, 0, 0, 0.001, 0, 0] with upm=1000. - if (this.glyphData.FontMatrix is double[] fm) - { - float upm = this.UnitsPerEm; - scale *= new Vector2((float)(fm[0] * upm), (float)(fm[3] * upm)); - } - - Vector2 scaledOffset = this.Offset * scale; this.glyphData.RenderTo(target, glyphOrigin, scale, scaledOffset, transform); + target.CompleteOutline(); } - - target.EndGlyph(); - this.RenderDecorationsTo(renderer, decorationOrigin, mode, rotation, scaledPPEM, options); + finally + { + target.Release(); + } + } + else + { + this.glyphData.RenderTo(renderer, glyphOrigin, scale, scaledOffset, transform); } } } diff --git a/src/SixLabors.Fonts/Tables/Cff/TransformingGlyphRenderer.cs b/src/SixLabors.Fonts/Tables/Cff/TransformingGlyphRenderer.cs index 30243c39b..3ae8f5e46 100644 --- a/src/SixLabors.Fonts/Tables/Cff/TransformingGlyphRenderer.cs +++ b/src/SixLabors.Fonts/Tables/Cff/TransformingGlyphRenderer.cs @@ -130,8 +130,8 @@ public readonly TextDecorations EnabledDecorations() => this.renderer.EnabledDecorations(); /// - public readonly void SetDecoration(TextDecorations textDecorations, Vector2 start, Vector2 end, float thickness) - => this.renderer.SetDecoration(textDecorations, this.Transform(start), this.Transform(end), thickness); + public readonly void SetDecoration(TextDecorations textDecorations, Vector2 start, Vector2 end, float thickness, ReadOnlyMemory intersections) + => this.renderer.SetDecoration(textDecorations, this.Transform(start), this.Transform(end), thickness, intersections); /// /// Applies the scale, offset, transform matrix, and Y-axis inversion to the given point. diff --git a/src/SixLabors.Fonts/Tables/General/CMapTable.cs b/src/SixLabors.Fonts/Tables/General/CMapTable.cs index a1713f0cc..3218afa18 100644 --- a/src/SixLabors.Fonts/Tables/General/CMapTable.cs +++ b/src/SixLabors.Fonts/Tables/General/CMapTable.cs @@ -100,23 +100,32 @@ public bool TryGetGlyphId(CodePoint codePoint, CodePoint? nextCodePoint, out ush /// /// The code point to look up. /// When this method returns, contains the glyph ID if found. - /// if a non-zero glyph ID was found; otherwise, . + /// if a glyph ID was found; otherwise, . private bool TryGetGlyphId(CodePoint codePoint, out ushort glyphId) { + bool foundFallback = false; + foreach (CMapSubTable t in this.Tables) { // Keep looking until we have an index that's not the fallback. // Regardless of the encoding scheme, character codes that do // not correspond to any glyph in the font should be mapped to glyph index 0. // The glyph at this location must be a special glyph representing a missing character, commonly known as .notdef. - if (t.TryGetGlyphId(codePoint, out glyphId) && glyphId > 0) + if (!t.TryGetGlyphId(codePoint, out glyphId)) + { + continue; + } + + if (glyphId > 0) { return true; } + + foundFallback = true; } glyphId = 0; - return false; + return foundFallback; } /// diff --git a/src/SixLabors.Fonts/Tables/General/Colr/ColrTable.cs b/src/SixLabors.Fonts/Tables/General/Colr/ColrTable.cs index 9790ce16c..b80794565 100644 --- a/src/SixLabors.Fonts/Tables/General/Colr/ColrTable.cs +++ b/src/SixLabors.Fonts/Tables/General/Colr/ColrTable.cs @@ -58,9 +58,20 @@ internal class ColrTable : Table private readonly DeltaSetIndexMap[]? deltaSetIndexMap; /// - /// Cache of resolved paint objects keyed by their COLR-relative offset. + /// The raw COLR table data used to resolve paint subtrees on demand, or + /// when no v1 paint data is present. Paint graphs are decoded + /// lazily per base glyph on first use: fonts can carry thousands of color glyphs and + /// eagerly walking every paint graph dominates font load time, while a typical document + /// renders only a handful of them. /// - private readonly Dictionary? paintCache; + private readonly byte[]? paintData; + + /// + /// Caches of paint objects, color lines, and affine matrices resolved from + /// so far, keyed by their COLR-relative offsets. Lazy resolution + /// locks this instance, so concurrent renders decode each subtree exactly once. + /// + private readonly PaintCaches paintCaches = new(); /// /// Initializes a new instance of the class for COLR v0 data only. @@ -84,7 +95,7 @@ public ColrTable( /// The COLR v1 clip list, or . /// The ItemVariationStore for variable font data, or . /// The DeltaSetIndexMap array, or . - /// The pre-loaded paint cache, or . + /// The raw COLR table data for lazy paint resolution, or . /// The COLR table version. public ColrTable( BaseGlyphRecord[] glyphRecords, @@ -94,7 +105,7 @@ public ColrTable( ClipList? clipList, ItemVariationStore? itemVariationStore, DeltaSetIndexMap[]? deltaSetIndexMap, - Dictionary? paintCache = null, + byte[]? paintData = null, int version = 1) { this.glyphRecords = glyphRecords; @@ -104,7 +115,7 @@ public ColrTable( this.clipList = clipList; this.itemVariationStore = itemVariationStore; this.deltaSetIndexMap = deltaSetIndexMap; - this.paintCache = paintCache; + this.paintData = paintData; this.Version = version; } @@ -152,14 +163,14 @@ internal float ResolveDelta(GlyphVariationProcessor? processor, uint varIdx) /// The loaded , or if the table is not present. public static ColrTable? Load(FontReader fontReader) { - if (!fontReader.TryGetReaderAtTablePosition(TableName, out BigEndianBinaryReader? binaryReader)) + if (!fontReader.TryGetReaderAtTablePosition(TableName, out BigEndianBinaryReader? binaryReader, out TableHeader? header)) { return null; } using (binaryReader) { - return Load(binaryReader); + return Load(binaryReader, header.Length); } } @@ -211,7 +222,7 @@ public bool ContainsColorV0Glyph(ushort glyphId) /// public bool ContainsColorV1Glyph(ushort glyphId) { - if (this.baseGlyphList is null || this.layerList is null || this.paintCache is null) + if (this.baseGlyphList is null || this.layerList is null || this.paintData is null) { return false; // No COLR v1 data } @@ -265,7 +276,7 @@ internal bool TryGetColrV1Layers( { layers = null; - if (this.baseGlyphList is null || this.layerList is null || this.paintCache is null) + if (this.baseGlyphList is null || this.layerList is null || this.paintData is null) { return false; // No COLR v1 data } @@ -276,7 +287,7 @@ internal bool TryGetColrV1Layers( return false; } - if (!this.paintCache.TryGetValue(rootOff, out Paint? root) || root is null) + if (!this.TryGetPaint(rootOff, out Paint? root)) { return false; } @@ -348,7 +359,7 @@ private void FlattenPaintToLayers( continue; } - if (this.paintCache!.TryGetValue(off, out Paint? child) && child is not null) + if (this.TryGetPaint(off, out Paint? child)) { this.FlattenPaintToLayers(child, currentGlyphId, glyphTransform, paintTransform, transformPaint, compositeMode, processor, outLayers); } @@ -361,7 +372,7 @@ private void FlattenPaintToLayers( { // Resolve the referenced glyph's root paint and recurse through its own bindings. if (this.TryGetRootPaintOffset(pcg.GlyphId, out uint off) && off != 0 - && this.paintCache!.TryGetValue(off, out Paint? colrRoot) && colrRoot is not null) + && this.TryGetPaint(off, out Paint? colrRoot)) { this.FlattenPaintToLayers(colrRoot, null, glyphTransform, Matrix3x2.Identity, false, compositeMode, processor, outLayers); } @@ -701,6 +712,36 @@ private static CompositeMode MapCompositeMode(ColrCompositeMode? mode) _ => CompositeMode.SrcOver, }; + /// + /// Attempts to resolve the paint at the specified COLR-relative offset, decoding it and + /// its subtree from the raw table data on first use. Resolution locks the paint caches, + /// so concurrent renders decode each subtree exactly once and later lookups are + /// dictionary hits. + /// + /// The COLR-relative offset of the paint table. + /// When this method returns, contains the resolved paint, if found; otherwise, . + /// if the paint was resolved; otherwise, . + private bool TryGetPaint(uint paintOffset, [NotNullWhen(true)] out Paint? paint) + { + lock (this.paintCaches) + { + if (this.paintCaches.PaintCache.TryGetValue(paintOffset, out paint)) + { + return paint is not null; + } + + if (this.paintData is null) + { + paint = null; + return false; + } + + using BigEndianBinaryReader reader = new(new MemoryStream(this.paintData, false), leaveOpen: false); + paint = LoadPaintAt(reader, paintOffset, this.layerList, this.paintCaches); + return paint is not null; + } + } + /// /// Attempts to retrieve the paint table offset associated with the specified glyph ID. /// @@ -790,11 +831,21 @@ private bool TryGetClipBox(ushort glyphId, GlyphVariationProcessor? processor, o } /// - /// Loads the COLR table from the specified binary reader. + /// Loads the COLR table from the specified binary reader, taking the table length from + /// the remaining stream extent. /// /// The big-endian binary reader positioned at the start of the COLR table. /// The loaded . public static ColrTable Load(BigEndianBinaryReader reader) + => Load(reader, checked((uint)(reader.BaseStream.Length - reader.StartOfStream))); + + /// + /// Loads the COLR table from the specified binary reader. + /// + /// The big-endian binary reader positioned at the start of the COLR table. + /// The length of the COLR table in bytes. + /// The loaded . + public static ColrTable Load(BigEndianBinaryReader reader, uint tableLength) { // HEADER @@ -872,7 +923,7 @@ public static ColrTable Load(BigEndianBinaryReader reader) BaseGlyphList? baseGlyphList = null; LayerList? layerList = null; ClipList? clipList = null; - Dictionary? paintCache = null; + byte[]? paintData = null; if (version == 1) { @@ -880,7 +931,14 @@ public static ColrTable Load(BigEndianBinaryReader reader) layerList = LayerList.Load(reader, layerListOffset); clipList = ClipList.Load(reader, clipListOffset); - paintCache = LoadPaintRoots(reader, baseGlyphList, layerList); + // Snapshot the table in one bulk read so paint graphs can be decoded lazily per + // base glyph on first use. Fonts can carry thousands of color glyphs; walking + // every paint graph here would dominate font load time while a typical document + // renders only a handful of them. + long restore = reader.BaseStream.Position; + reader.Seek(0, SeekOrigin.Begin); + paintData = reader.ReadBytes(checked((int)tableLength)); + reader.BaseStream.Position = restore; } ItemVariationStore? itemVariationStore = itemVariationStoreOffset != 0 @@ -891,48 +949,7 @@ public static ColrTable Load(BigEndianBinaryReader reader) ? DeltaSetIndexMap.Load(reader, varIndexMapOffset) : null; - return new ColrTable(glyphs, layerRecs, baseGlyphList, layerList, clipList, itemVariationStore, deltaSetIndexMap, paintCache, 1); - } - - /// - /// Eagerly loads and caches all paint objects referenced by the BaseGlyphList and LayerList. - /// - /// The binary reader. - /// The base glyph list, or . - /// The layer list, or . - /// A dictionary mapping paint offsets to their resolved paint objects. - private static Dictionary LoadPaintRoots( - BigEndianBinaryReader reader, - BaseGlyphList? baseGlyphList, - LayerList? layerList) - { - PaintCaches caches = new(); - - // 1) Root paints from BaseGlyphList - if (baseGlyphList is not null) - { - 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 caches.PaintCache; + return new ColrTable(glyphs, layerRecs, baseGlyphList, layerList, clipList, itemVariationStore, deltaSetIndexMap, paintData, 1); } /// diff --git a/src/SixLabors.Fonts/Tables/General/OS2Table.cs b/src/SixLabors.Fonts/Tables/General/OS2Table.cs index 48eb2e740..e27356e62 100644 --- a/src/SixLabors.Fonts/Tables/General/OS2Table.cs +++ b/src/SixLabors.Fonts/Tables/General/OS2Table.cs @@ -368,6 +368,11 @@ internal enum FontStyleSelection : ushort /// public FontStyleSelection FontStyle { get; } + /// + /// Gets the visual weight class of the font. + /// + public ushort WeightClass => this.weightClass; + /// /// Gets the typographic ascender value. /// diff --git a/src/SixLabors.Fonts/Tables/TrueType/Hinting/TrueTypeInterpreter.cs b/src/SixLabors.Fonts/Tables/TrueType/Hinting/TrueTypeInterpreter.cs index 2df3fd2ff..af5e3d834 100644 --- a/src/SixLabors.Fonts/Tables/TrueType/Hinting/TrueTypeInterpreter.cs +++ b/src/SixLabors.Fonts/Tables/TrueType/Hinting/TrueTypeInterpreter.cs @@ -21,10 +21,11 @@ namespace SixLabors.Fonts.Tables.TrueType.Hinting; /// /// This implementation matches the behavior of FreeType's v40 subpixel hinting interpreter, /// with horizontal hinting disabled and full vertical TrueType instruction processing preserved. -/// It follows the v40 model in which outlines are adjusted primarily along the Y-axis and -/// instructions operate without backward compatibility constraints. This corresponds to -/// FreeType's configuration where TT_CONFIG_OPTION_SUBPIXEL_HINTING selects the -/// minimal (v40) engine and backward_compatibility is forced to zero. +/// Backward compatibility mode is active by default, exactly as in FreeType's minimal (v40) +/// engine: X-axis moves are ignored, no point moves after both IUP calls, and SHPIX/DELTAP +/// execute only in their gated forms. Fonts opt out per FreeType's rules by executing +/// INSTCTRL selector 3 (the native ClearType waiver) in the prep program, or temporarily +/// within a single glyph program. /// /// /// @@ -790,9 +791,12 @@ private void Execute(StackInstructionStream stream, bool inFunction, bool allowF int value = this.stack.Pop(); // FreeType restricts selectors 1-2 to the prep (CVT) program only. - // Selector 3 (NativeClearType) can also be set during prep. - // Glyph programs cannot modify instruction control flags. - if (selector is >= 1 and <= 3 && !this.inGlyphProgram) + // Selector 3 (NativeClearType) is additionally honored inside glyph + // programs: native ClearType fonts may sign the backward-compatibility + // waiver for a single glyph (FreeType Ins_INSTCTRL, tt_coderange_glyph). + // Because the graphics state is restored from the prep snapshot before + // each glyph program, such a toggle is naturally temporary. + if (selector is >= 1 and <= 3 && (!this.inGlyphProgram || selector == 3)) { int bit = 1 << (selector - 1); @@ -2103,6 +2107,16 @@ private void Execute(StackInstructionStream stream, bool inFunction, bool allowF result |= 1 << 18; } + // Selector bit 12: ClearType hinting and grayscale rendering. + // FreeType sets this whenever the render mode is not monochrome or LCD; + // our rasterization is always symmetric grayscale, so it is always set. + // ClearType-era prep programs branch on this to select grayscale-safe + // hinting instead of LCD-specific pixel tweaks. + if ((selector & 0x1000) != 0) + { + result |= 1 << 19; + } + this.stack.Push(result); } diff --git a/src/SixLabors.Fonts/Tables/TrueType/TrueTypeGlyphMetrics.cs b/src/SixLabors.Fonts/Tables/TrueType/TrueTypeGlyphMetrics.cs index d1ba1c13a..98ceef5be 100644 --- a/src/SixLabors.Fonts/Tables/TrueType/TrueTypeGlyphMetrics.cs +++ b/src/SixLabors.Fonts/Tables/TrueType/TrueTypeGlyphMetrics.cs @@ -132,141 +132,122 @@ internal override FontGlyphMetrics CloneForRendering(TextRun textRun) internal GlyphVector GetOutline() => this.vector; /// - internal override void RenderTo( + internal override void RenderOutlineTo( IGlyphRenderer renderer, - int graphemeIndex, Vector2 glyphOrigin, - Vector2 decorationOrigin, GlyphLayoutMode mode, - TextOptions options) + float scaledPPEM, + HintingMode hintingMode) { - // https://www.unicode.org/faq/unsup_char.html - if (ShouldSkipGlyphRendering(this.CodePoint)) + Matrix3x2 transform = this.GetOutlineTransform(mode); + GlyphVector scaledVector = this.scaledVectorCache.GetOrAdd(scaledPPEM, _ => { - return; - } + // Create a scaled deep copy of the vector so that we do not alter + // the globally cached instance. + GlyphVector clone = GlyphVector.DeepClone(this.vector); + Vector2 scale = new Vector2(scaledPPEM) / this.ScaleFactor; + + Matrix3x2 matrix = Matrix3x2.CreateScale(scale); + matrix.Translation = this.Offset * scale; + GlyphVector.TransformInPlace(ref clone, matrix); - float pointSize = this.TextRun.Font?.Size ?? options.Font.Size; - float dpi = options.Dpi; + float pixelSize = scaledPPEM / 72F; + this.FontMetrics.ApplyTrueTypeHinting(this.GetHintingMode(hintingMode), this, ref clone, scale, pixelSize); - glyphOrigin *= dpi; - decorationOrigin *= dpi; - float scaledPPEM = this.GetScaledSize(pointSize, dpi); + // The shared outline transform applies synthetic oblique before layout rotation. + // Both happen after hinting so the hinter always receives the upright outline. + GlyphVector.TransformInPlace(ref clone, transform); + return clone; + }); - Matrix3x2 rotation = GetRotationMatrix(mode); - FontRectangle box = this.GetBoundingBox(mode, glyphOrigin, scaledPPEM); - GlyphRendererParameters parameters = new(this, this.TextRun, pointSize, dpi, mode, graphemeIndex); + IList controlPoints = scaledVector.ControlPoints; + IReadOnlyList endPoints = scaledVector.EndPoints; - // Synthesize a bold (faux bold) weight when requested but unavailable by dilating the - // outline through a decorator. Decorations continue to use the original renderer. float boldStrength = this.GetSyntheticBoldStrength(scaledPPEM); - IGlyphRenderer target = boldStrength > 0F ? new EmboldeningGlyphRenderer(renderer, boldStrength) : renderer; + EmboldeningGlyphRenderer? emboldening = null; + IGlyphRenderer target = renderer; + if (boldStrength > 0F) + { + emboldening = EmboldeningGlyphRenderer.Rent(renderer, boldStrength); + target = emboldening; + } - if (target.BeginGlyph(in box, in parameters)) + try { - if (!UnicodeUtility.ShouldRenderWhiteSpaceOnly(this.CodePoint)) + int endOfContour = -1; + for (int i = 0; i < scaledVector.EndPoints.Count; i++) { - GlyphVector scaledVector = this.scaledVectorCache.GetOrAdd(scaledPPEM, _ => - { - // Create a scaled deep copy of the vector so that we do not alter - // the globally cached instance. - GlyphVector clone = GlyphVector.DeepClone(this.vector); - Vector2 scale = new Vector2(scaledPPEM) / this.ScaleFactor; + target.BeginFigure(); + int startOfContour = endOfContour + 1; + endOfContour = endPoints[i]; - Matrix3x2 matrix = Matrix3x2.CreateScale(scale); - matrix.Translation = this.Offset * scale; - GlyphVector.TransformInPlace(ref clone, matrix); + Vector2 prev; + Vector2 curr = (YInverter * controlPoints[endOfContour].Point) + glyphOrigin; + Vector2 next = (YInverter * controlPoints[startOfContour].Point) + glyphOrigin; - float pixelSize = scaledPPEM / 72F; - this.FontMetrics.ApplyTrueTypeHinting(this.GetHintingMode(options.HintingMode), this, ref clone, scale, pixelSize); - - // Synthesize an oblique (faux italic) slant when requested but unavailable. - // Applied after hinting so the hinter operates on the upright outline. - float skew = this.GetObliqueSkew(); - if (skew != 0F) + if (controlPoints[endOfContour].OnCurve) + { + target.MoveTo(curr); + } + else + { + if (controlPoints[startOfContour].OnCurve) { - GlyphVector.TransformInPlace(ref clone, CreateObliqueMatrix(skew)); + target.MoveTo(next); } + else + { + // If both first and last points are off-curve, start at their middle. + Vector2 startPoint = (curr + next) * .5F; + target.MoveTo(startPoint); + } + } - // Rotation must happen after hinting. - GlyphVector.TransformInPlace(ref clone, rotation); - return clone; - }); - - IList controlPoints = scaledVector.ControlPoints; - IReadOnlyList endPoints = scaledVector.EndPoints; - - int endOfContour = -1; - for (int i = 0; i < scaledVector.EndPoints.Count; i++) + int length = endOfContour - startOfContour + 1; + for (int p = 0; p < length; p++) { - target.BeginFigure(); - int startOfContour = endOfContour + 1; - endOfContour = endPoints[i]; - - Vector2 prev; - Vector2 curr = (YInverter * controlPoints[endOfContour].Point) + glyphOrigin; - Vector2 next = (YInverter * controlPoints[startOfContour].Point) + glyphOrigin; - - if (controlPoints[endOfContour].OnCurve) + prev = curr; + curr = next; + int currentIndex = startOfContour + p; + int nextIndex = startOfContour + ((p + 1) % length); + int prevIndex = startOfContour + ((length + p - 1) % length); + next = (YInverter * controlPoints[nextIndex].Point) + glyphOrigin; + + if (controlPoints[currentIndex].OnCurve) { - target.MoveTo(curr); + // This is a straight line. + target.LineTo(curr); } else { - if (controlPoints[startOfContour].OnCurve) - { - target.MoveTo(next); - } - else + Vector2 prev2 = prev; + Vector2 next2 = next; + + if (!controlPoints[prevIndex].OnCurve) { - // If both first and last points are off-curve, start at their middle. - Vector2 startPoint = (curr + next) * .5F; - target.MoveTo(startPoint); + prev2 = (curr + prev) * .5F; + target.LineTo(prev2); } - } - - int length = endOfContour - startOfContour + 1; - for (int p = 0; p < length; p++) - { - prev = curr; - curr = next; - int currentIndex = startOfContour + p; - int nextIndex = startOfContour + ((p + 1) % length); - int prevIndex = startOfContour + ((length + p - 1) % length); - next = (YInverter * controlPoints[nextIndex].Point) + glyphOrigin; - if (controlPoints[currentIndex].OnCurve) + if (!controlPoints[nextIndex].OnCurve) { - // This is a straight line. - target.LineTo(curr); + next2 = (curr + next) * .5F; } - else - { - Vector2 prev2 = prev; - Vector2 next2 = next; - if (!controlPoints[prevIndex].OnCurve) - { - prev2 = (curr + prev) * .5F; - target.LineTo(prev2); - } - - if (!controlPoints[nextIndex].OnCurve) - { - next2 = (curr + next) * .5F; - } - - target.LineTo(prev2); - target.QuadraticBezierTo(curr, next2); - } + target.LineTo(prev2); + target.QuadraticBezierTo(curr, next2); } - - target.EndFigure(); } + + target.EndFigure(); } - target.EndGlyph(); - this.RenderDecorationsTo(renderer, decorationOrigin, mode, rotation, scaledPPEM, options); + // Emit the completed fill group before FontGlyphMetrics ends the glyph. + emboldening?.CompleteOutline(); + } + finally + { + emboldening?.Release(); } } } diff --git a/src/SixLabors.Fonts/TextBlock.Visitors.cs b/src/SixLabors.Fonts/TextBlock.Visitors.cs index 522d2d43d..991e5a158 100644 --- a/src/SixLabors.Fonts/TextBlock.Visitors.cs +++ b/src/SixLabors.Fonts/TextBlock.Visitors.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Numerics; using SixLabors.Fonts.Rendering; using SixLabors.Fonts.Unicode; @@ -802,7 +803,11 @@ public readonly void Visit(in GlyphLayout glyph) return; } - glyph.Glyph.RenderTo(this.renderer, glyph.GraphemeIndex, glyph.GlyphOrigin, glyph.DecorationOrigin, glyph.LayoutMode, this.options); + // The laid-out advance includes layout-time spacing such as tracking and + // justification, so decorations span the full layout cell rather than the + // glyph's own metric advance. + Vector2 layoutAdvance = new(glyph.AdvanceX, glyph.AdvanceY); + glyph.Glyph.RenderTo(this.renderer, glyph.GraphemeIndex, glyph.GlyphOrigin, glyph.DecorationOrigin, layoutAdvance, glyph.LayoutMode, this.options); } /// diff --git a/src/SixLabors.Fonts/TextBlock.cs b/src/SixLabors.Fonts/TextBlock.cs index 107379e37..20c96ff01 100644 --- a/src/SixLabors.Fonts/TextBlock.cs +++ b/src/SixLabors.Fonts/TextBlock.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. using System.Numerics; @@ -53,6 +53,12 @@ public TextBlock(ReadOnlySpan text, TextOptions options) /// internal TextOptions Options { get; } + /// + /// Gets a value indicating whether underline and overline decorations skip over glyph ink + /// when this block is rendered. + /// + public TextDecorationSkipInk TextDecorationSkipInk => this.Options.TextDecorationSkipInk; + /// /// Gets the prepared logical line and line break opportunities. /// @@ -145,6 +151,24 @@ public FontRectangle MeasureRenderableBounds(float wrappingLength) public ReadOnlyMemory GetGlyphMetrics(float wrappingLength) => this.GetGlyphMetricsArray(wrappingLength); + /// + /// Gets the x-axis intervals where the laid-out glyph outlines cross a horizontal band. + /// Glyphs whose bounds do not touch the band skip outline decoding entirely. + /// + /// The wrapping length in pixels. Use -1 to disable wrapping. + /// One edge of the horizontal band. + /// The other edge of the horizontal band. + /// + /// A read-only memory region containing merged, x-sorted interval pairs + /// (start, end, start, end, ...); empty when no outline crosses the band. + /// + public ReadOnlyMemory GetIntersections(float wrappingLength, float lowerLimit, float upperLimit) + { + GlyphIntersectionCollector collector = new(lowerLimit, upperLimit); + this.RenderTo(collector, wrappingLength); + return collector.BuildIntersections(); + } + /// /// Gets the positioned metrics of each laid-out grapheme. /// diff --git a/src/SixLabors.Fonts/TextDecorationOptions.cs b/src/SixLabors.Fonts/TextDecorationOptions.cs new file mode 100644 index 000000000..804f6fd73 --- /dev/null +++ b/src/SixLabors.Fonts/TextDecorationOptions.cs @@ -0,0 +1,23 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts; + +/// +/// Represents overrides for the geometry of a single text decoration line, allowing a +/// to replace the values that would otherwise be derived from the font metrics. +/// +/// +/// Values are expressed in device pixels. Any property left falls back to the +/// font's metric-derived value. Further overrides (for example a position override) can be added as +/// new nullable properties without changing the +/// contract. +/// +public class TextDecorationOptions +{ + /// + /// Gets or sets the thickness of the decoration line, in device pixels, or + /// to use the font's metric-derived thickness. + /// + public float? Thickness { get; set; } +} diff --git a/src/SixLabors.Fonts/TextDecorationSkipInk.cs b/src/SixLabors.Fonts/TextDecorationSkipInk.cs new file mode 100644 index 000000000..ab3130e14 --- /dev/null +++ b/src/SixLabors.Fonts/TextDecorationSkipInk.cs @@ -0,0 +1,23 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts; + +/// +/// Identifies whether underline and overline decorations are interrupted where they would +/// otherwise cross glyph ink, mirroring the CSS text-decoration-skip-ink property. +/// Strikethrough decorations always cross the ink regardless of this setting. +/// +public enum TextDecorationSkipInk : byte +{ + /// + /// Underlines and overlines skip over glyph ink, leaving gaps around descenders and + /// ascenders that cross the decoration. + /// + Auto = 0, + + /// + /// Decorations are drawn continuously, crossing any glyph ink in their path. + /// + None = 1 +} diff --git a/src/SixLabors.Fonts/TextLayout.cs b/src/SixLabors.Fonts/TextLayout.cs index 905199f01..d431f2c01 100644 --- a/src/SixLabors.Fonts/TextLayout.cs +++ b/src/SixLabors.Fonts/TextLayout.cs @@ -34,15 +34,15 @@ public static IReadOnlyList BuildTextRuns(ReadOnlySpan text, Text if (options.TextRuns is null || options.TextRuns.Count == 0) { - return new TextRun[] + TextRun textRun = new() { - new() - { - Start = 0, - End = text.GetGraphemeCount(), - Font = options.Font - } + Start = 0, + End = text.GetGraphemeCount(), + Font = options.Font }; + + textRun.ResolveFontWeight(options.FontWeight); + return [textRun]; } List textRuns = []; @@ -90,6 +90,11 @@ public static IReadOnlyList BuildTextRuns(ReadOnlySpan text, Text }); } + foreach (TextRun textRun in textRuns) + { + textRun.ResolveFontWeight(options.FontWeight); + } + return textRuns; } @@ -189,7 +194,7 @@ or BidiCharacterType.FirstStrongIsolate textRun, codePointIndex); - complete &= positionings.TryAdd(textRun.Font!, substitutions); + complete &= positionings.TryAdd(textRun.ResolvedFont, substitutions); textRunIndex++; continue; } @@ -202,7 +207,7 @@ or BidiCharacterType.FirstStrongIsolate ref codePointIndex, ref bidiRunIndex, false, - textRun.Font!, + textRun.ResolvedFont, bidiRuns, bidiMap, substitutions, @@ -248,13 +253,14 @@ or BidiCharacterType.FirstStrongIsolate { TextRun textRun = textRuns[i]; - if (textRun.Font == lastFont) + Font font = textRun.ResolvedFont; + if (font == lastFont) { continue; } - textRun.Font!.FontMetrics.UpdatePositions(positionings); - lastFont = textRun.Font; + font.FontMetrics.UpdatePositions(positionings); + lastFont = font; } foreach (Font font in fallbackFonts) diff --git a/src/SixLabors.Fonts/TextMeasurer.cs b/src/SixLabors.Fonts/TextMeasurer.cs index 13e27df39..4e1f292cc 100644 --- a/src/SixLabors.Fonts/TextMeasurer.cs +++ b/src/SixLabors.Fonts/TextMeasurer.cs @@ -1,6 +1,10 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Diagnostics.CodeAnalysis; +using System.Numerics; +using SixLabors.Fonts.Rendering; + namespace SixLabors.Fonts; /// @@ -89,6 +93,145 @@ public static FontRectangle MeasureRenderableBounds(ReadOnlySpan text, Tex return block.MeasureRenderableBounds(options.WrappingLength); } + /// + /// Measures the logical advance of a single glyph, identified by its glyph id, in pixel units. + /// + /// + /// The advance is computed directly from the font's cached per-glyph metrics without decoding + /// outlines or running the layout engine. is the glyph's + /// baseline pen position. For horizontal layouts the advance rectangle spans the same line + /// box the layout engine builds for a single glyph (the em box, ascender-balanced against the + /// font's declared line height) over the glyph's advance width. For vertical layouts the + /// rectangle is centered on the origin and extends downward by the advance the layout + /// direction consumes; line-level policies such as column centering and line spacing only + /// apply when text is laid out. + /// + /// The glyph identifier within the font face referenced by . + /// The glyph options, including the font, origin, and layout mode. + /// + /// The logical advance rectangle of the glyph if it was to be rendered, or + /// when the font does not contain the glyph or the glyph + /// never renders. + /// + public static FontRectangle MeasureAdvance(ushort glyphId, GlyphOptions options) + { + Guard.NotNull(options, nameof(options)); + + return TryGetMeasurableGlyphMetrics(glyphId, options, out FontGlyphMetrics? metrics) + ? GetGlyphAdvance(metrics, options) + : FontRectangle.Empty; + } + + /// + /// Measures the rendered bounds of a single glyph, identified by its glyph id, in pixel units. + /// + /// + /// The bounds are computed directly from the font's cached per-glyph metrics without decoding + /// outlines or running the layout engine, and are identical to the bounding box the renderer + /// reports for the same glyph and options through + /// . + /// + /// The glyph identifier within the font face referenced by . + /// The glyph options, including the font, origin, and layout mode. + /// + /// The rendered bounds of the glyph at if it was to be + /// rendered, or when the font does not contain the glyph or + /// the glyph never renders. + /// + public static FontRectangle MeasureBounds(ushort glyphId, GlyphOptions options) + { + Guard.NotNull(options, nameof(options)); + + return TryGetMeasurableGlyphMetrics(glyphId, options, out FontGlyphMetrics? metrics) + ? GetGlyphBounds(metrics, options) + : FontRectangle.Empty; + } + + /// + /// Measures the full renderable bounds of a single glyph, identified by its glyph id, in pixel units. + /// + /// + /// The bounds are computed directly from the font's cached per-glyph metrics without decoding + /// outlines or running the layout engine. + /// + /// The glyph identifier within the font face referenced by . + /// The glyph options, including the font, origin, and layout mode. + /// + /// The union of the logical advance rectangle and the rendered bounds of the glyph if it was + /// to be rendered, or when the font does not contain the + /// glyph or the glyph never renders. + /// + public static FontRectangle MeasureRenderableBounds(ushort glyphId, GlyphOptions options) + { + Guard.NotNull(options, nameof(options)); + + return TryGetMeasurableGlyphMetrics(glyphId, options, out FontGlyphMetrics? metrics) + ? FontRectangle.Union(GetGlyphAdvance(metrics, options), GetGlyphBounds(metrics, options)) + : FontRectangle.Empty; + } + + /// + /// Measures the union of logical glyph advances for positioned glyphs in pixel units. + /// + /// + /// Each glyph is measured at its own run origin exactly as + /// + /// renders it; is replaced per glyph and restored. Glyph ids + /// the font does not contain and glyphs that never render are skipped, matching renderer + /// behavior. + /// + /// The positioned glyphs. + /// The glyph options, including the font and layout mode. + /// + /// The union of the logical advance rectangles of the run if it was to be rendered, or + /// when no glyph in the run participates in rendering. + /// + public static FontRectangle MeasureAdvance(GlyphRun glyphRun, GlyphOptions options) + => MeasureGlyphRun(glyphRun, options, static (metrics, options) => GetGlyphAdvance(metrics, options)); + + /// + /// Measures the union of rendered glyph bounds for positioned glyphs in pixel units. + /// + /// + /// Each glyph is measured at its own run origin exactly as + /// + /// renders it; is replaced per glyph and restored. The + /// result matches the union of the bounding boxes the renderer reports for the same run and + /// options. Glyph ids the font does not contain and glyphs that never render are skipped, + /// matching renderer behavior. + /// + /// The positioned glyphs. + /// The glyph options, including the font and layout mode. + /// + /// The union of the rendered glyph bounds of the run if it was to be rendered, or + /// when no glyph in the run participates in rendering. + /// + public static FontRectangle MeasureBounds(GlyphRun glyphRun, GlyphOptions options) + => MeasureGlyphRun(glyphRun, options, static (metrics, options) => GetGlyphBounds(metrics, options)); + + /// + /// Measures the full renderable bounds of positioned glyphs in pixel units. + /// + /// + /// Each glyph is measured at its own run origin exactly as + /// + /// renders it; is replaced per glyph and restored. Glyph ids + /// the font does not contain and glyphs that never render are skipped, matching renderer + /// behavior. + /// + /// The positioned glyphs. + /// The glyph options, including the font and layout mode. + /// + /// The union of the logical advance rectangles and the rendered glyph bounds of the run if it + /// was to be rendered, or when no glyph in the run + /// participates in rendering. + /// + public static FontRectangle MeasureRenderableBounds(GlyphRun glyphRun, GlyphOptions options) + => MeasureGlyphRun( + glyphRun, + options, + static (metrics, options) => FontRectangle.Union(GetGlyphAdvance(metrics, options), GetGlyphBounds(metrics, options))); + /// public static ReadOnlyMemory GetGlyphMetrics(string text, TextOptions options) => GetGlyphMetrics(text.AsSpan(), options); @@ -110,6 +253,165 @@ public static ReadOnlyMemory GetGlyphMetrics(ReadOnlySpan te return block.GetGlyphMetrics(options.WrappingLength); } + /// + /// Gets the positioned metrics of a single glyph, identified by its glyph id, in pixel units. + /// + /// + /// The metrics are computed directly from the font's cached per-glyph metrics without + /// decoding outlines or running the layout engine, positioned at + /// . The entry's grapheme index is + /// and its string index is 0: glyph ids carry + /// no source text, so the index identifies the glyph within the measured input instead. A + /// glyph the font does not contain, or one that never renders, produces an entry with empty + /// rectangles. + /// + /// The glyph identifier within the font face referenced by . + /// The glyph options, including the font, origin, and layout mode. + /// The positioned metrics entry of the glyph if it was to be rendered. + public static GlyphMetrics GetGlyphMetrics(ushort glyphId, GlyphOptions options) + { + Guard.NotNull(options, nameof(options)); + + return CreateGlyphMetrics(glyphId, options, options.GraphemeIndex, index: 0); + } + + /// + /// Gets the positioned metrics of each glyph in a positioned run in pixel units. + /// + /// + /// The metrics are computed directly from the font's cached per-glyph metrics without + /// decoding outlines or running the layout engine. Each glyph is measured at its own run + /// origin exactly as + /// + /// renders it; is replaced per glyph and restored. One + /// entry is returned per input glyph so results correlate with run indices: each entry's + /// grapheme index is plus the run index and its + /// string index is the run index. Glyph ids the font does not contain, and glyphs that + /// never render, produce entries with empty rectangles. + /// + /// The positioned glyphs. + /// The glyph options, including the font and layout mode. + /// A read-only memory region containing one positioned metrics entry per input glyph. + public static ReadOnlyMemory GetGlyphMetrics(GlyphRun glyphRun, GlyphOptions options) + { + Guard.NotNull(glyphRun, nameof(glyphRun)); + Guard.NotNull(options, nameof(options)); + + if (glyphRun.Count == 0) + { + return ReadOnlyMemory.Empty; + } + + ReadOnlySpan glyphIds = glyphRun.GlyphIds.Span; + ReadOnlySpan origins = glyphRun.Origins.Span; + Vector2 originalOrigin = options.Origin; + int originalGraphemeIndex = options.GraphemeIndex; + + GlyphMetrics[] metrics = new GlyphMetrics[glyphIds.Length]; + try + { + for (int i = 0; i < glyphIds.Length; i++) + { + options.Origin = origins[i]; + metrics[i] = CreateGlyphMetrics(glyphIds[i], options, originalGraphemeIndex + i, i); + } + } + finally + { + options.Origin = originalOrigin; + } + + return metrics; + } + + /// + public static ReadOnlyMemory GetIntersections(string text, TextOptions options, float lowerLimit, float upperLimit) + => GetIntersections(text.AsSpan(), options, lowerLimit, upperLimit); + + /// + /// Gets the x-axis intervals where the laid-out text's glyph outlines cross a horizontal + /// band, in pixel units. + /// + /// + /// The intervals are computed from the exact outline geometry the renderer would draw + /// (including hinting), so text decorations can be broken precisely around descenders. + /// Glyphs whose bounds do not touch the band skip outline decoding entirely. The band and + /// the returned x-values share the laid-out text's coordinate space. + /// + /// The text. + /// The text options. controls wrapping; use -1 to disable wrapping. + /// One edge of the horizontal band. + /// The other edge of the horizontal band. + /// + /// A read-only memory region containing merged, x-sorted interval pairs + /// (start, end, start, end, ...); empty when no outline crosses the band. + /// + public static ReadOnlyMemory GetIntersections(ReadOnlySpan text, TextOptions options, float lowerLimit, float upperLimit) + { + if (text.IsEmpty) + { + return ReadOnlyMemory.Empty; + } + + TextBlock block = new(text, options); + return block.GetIntersections(options.WrappingLength, lowerLimit, upperLimit); + } + + /// + /// Gets the x-axis intervals where a single glyph's outline, identified by its glyph id, + /// crosses a horizontal band, in pixel units. + /// + /// + /// The intervals are computed from the exact outline geometry the renderer would draw + /// (including hinting) at . The band and the returned + /// x-values share the glyph origin's coordinate space. + /// + /// The glyph identifier within the font face referenced by . + /// The glyph options, including the font, origin, and layout mode. + /// One edge of the horizontal band. + /// The other edge of the horizontal band. + /// + /// A read-only memory region containing merged, x-sorted interval pairs + /// (start, end, start, end, ...); empty when the outline does not cross the band. + /// + public static ReadOnlyMemory GetIntersections(ushort glyphId, GlyphOptions options, float lowerLimit, float upperLimit) + { + Guard.NotNull(options, nameof(options)); + + GlyphIntersectionCollector collector = new(lowerLimit, upperLimit); + TextRenderer.RenderTo(collector, glyphId, options); + return collector.BuildIntersections(); + } + + /// + /// Gets the x-axis intervals where positioned glyph outlines cross a horizontal band, + /// in pixel units. + /// + /// + /// The intervals are computed from the exact outline geometry the renderer would draw + /// (including hinting), each glyph at its own run origin, so text decorations can be broken + /// precisely around descenders. Glyphs whose bounds do not touch the band skip outline + /// decoding entirely. The band and the returned x-values share the run origins' coordinate + /// space. + /// + /// The positioned glyphs. + /// The glyph options, including the font and layout mode. + /// One edge of the horizontal band. + /// The other edge of the horizontal band. + /// + /// A read-only memory region containing merged, x-sorted interval pairs + /// (start, end, start, end, ...); empty when no outline crosses the band. + /// + public static ReadOnlyMemory GetIntersections(GlyphRun glyphRun, GlyphOptions options, float lowerLimit, float upperLimit) + { + Guard.NotNull(glyphRun, nameof(glyphRun)); + Guard.NotNull(options, nameof(options)); + + GlyphIntersectionCollector collector = new(lowerLimit, upperLimit); + TextRenderer.RenderTo(collector, glyphRun, options); + return collector.BuildIntersections(); + } + /// public static ReadOnlyMemory GetGraphemeMetrics(string text, TextOptions options) => GetGraphemeMetrics(text.AsSpan(), options); @@ -195,4 +497,190 @@ public static ReadOnlyMemory GetLineMetrics(ReadOnlySpan text TextBlock block = new(text, options); return block.GetLineMetrics(options.WrappingLength); } + + /// + /// Measures each positioned glyph of a run at its own origin and unions the results. + /// Mirrors : + /// is replaced per glyph and restored afterwards. + /// + /// The positioned glyphs. + /// The glyph options, including the font and layout mode. + /// The per-glyph measurement to union. + /// The union of the per-glyph measurements, or . + private static FontRectangle MeasureGlyphRun( + GlyphRun glyphRun, + GlyphOptions options, + Func measure) + { + Guard.NotNull(glyphRun, nameof(glyphRun)); + Guard.NotNull(options, nameof(options)); + + ReadOnlySpan glyphIds = glyphRun.GlyphIds.Span; + ReadOnlySpan origins = glyphRun.Origins.Span; + Vector2 originalOrigin = options.Origin; + + FontRectangle bounds = default; + bool hasBounds = false; + try + { + for (int i = 0; i < glyphIds.Length; i++) + { + if (!TryGetMeasurableGlyphMetrics(glyphIds[i], options, out FontGlyphMetrics? metrics)) + { + continue; + } + + options.Origin = origins[i]; + FontRectangle glyphBounds = measure(metrics, options); + bounds = hasBounds ? FontRectangle.Union(bounds, glyphBounds) : glyphBounds; + hasBounds = true; + } + } + finally + { + options.Origin = originalOrigin; + } + + return hasBounds ? bounds : FontRectangle.Empty; + } + + /// + /// Resolves the cached per-glyph metrics that participate in rendering, mirroring the + /// metric selection and skip rules the renderer applies so measurement and rendering + /// always agree. + /// + /// The glyph identifier within the font face referenced by . + /// The glyph options, including the font and layout mode. + /// Receives the glyph metrics when the glyph participates in rendering. + /// when the glyph participates in rendering; otherwise, . + private static bool TryGetMeasurableGlyphMetrics( + ushort glyphId, + GlyphOptions options, + [NotNullWhen(true)] out FontGlyphMetrics? metrics) + { + if (!options.Font.FontMetrics.TryGetGlyphMetrics( + glyphId, + options.TextAttributes, + options.TextDecorations, + options.LayoutMode, + options.ColorFontSupport, + out metrics) + || FontGlyphMetrics.ShouldSkipGlyphRendering(metrics.CodePoint)) + { + metrics = null; + return false; + } + + return true; + } + + /// + /// Builds one positioned metrics entry for a glyph id at . + /// Non-participating glyphs (ids the font does not contain, or glyphs that never render) + /// produce an entry with empty rectangles so run results stay index-correlated. + /// + /// The glyph identifier within the font face referenced by . + /// The glyph options, including the font, origin, and layout mode. + /// The grapheme index recorded on the entry. + /// The index of the glyph within the measured input, recorded as the entry's string index. + /// The positioned metrics entry. + private static GlyphMetrics CreateGlyphMetrics(ushort glyphId, GlyphOptions options, int graphemeIndex, int index) + { + if (!TryGetMeasurableGlyphMetrics(glyphId, options, out FontGlyphMetrics? metrics)) + { + return new GlyphMetrics( + default, + FontRectangle.Empty, + FontRectangle.Empty, + FontRectangle.Empty, + options.Font, + graphemeIndex, + index); + } + + FontRectangle advance = GetGlyphAdvance(metrics, options); + FontRectangle bounds = GetGlyphBounds(metrics, options); + return new GlyphMetrics( + metrics.CodePoint, + advance, + bounds, + FontRectangle.Union(advance, bounds), + options.Font, + graphemeIndex, + index); + } + + /// + /// Computes one glyph's rendered bounds at : the same + /// per-glyph layout mode and scaled size feed the same bounding-box computation the + /// renderer hands to . + /// + /// The glyph metrics. + /// The glyph options, including the font, origin, and layout mode. + /// The rendered glyph bounds. + private static FontRectangle GetGlyphBounds(FontGlyphMetrics metrics, GlyphOptions options) + => metrics.GetBoundingBox( + options.GetGlyphLayoutMode(metrics.CodePoint), + options.Origin, + metrics.GetScaledSize(options.Font.Size, options.Dpi)); + + /// + /// Computes one glyph's logical advance rectangle at , + /// mirroring the line-box construction the layout engine applies to a single glyph: the + /// line height is the em box, and the ascender is balanced by the delta that centers the + /// em box within the font's declared line height. Vertical layouts center the cell on the + /// origin and extend downward by the advance the layout direction consumes. + /// + /// The glyph metrics. + /// The glyph options, including the font, origin, and layout mode. + /// The logical advance rectangle. + private static FontRectangle GetGlyphAdvance(FontGlyphMetrics metrics, GlyphOptions options) + { + float scaledSize = metrics.GetScaledSize(options.Font.Size, options.Dpi); + Vector2 scale = new(scaledSize / metrics.ScaleFactor.X, scaledSize / metrics.ScaleFactor.Y); + Vector2 origin = options.Origin; + + // Match the layout engine's CSS-style line box: the em box is the line height, and + // the delta centers it within the font's declared line height. + // Reference: TextLayout.LineBreaking line-height calculation. + float emHeight = metrics.UnitsPerEm * scale.Y; + + switch (options.GetGlyphLayoutMode(metrics.CodePoint)) + { + case GlyphLayoutMode.Vertical: + { + float advanceWidth = metrics.AdvanceWidth * scale.X; + return new FontRectangle( + origin.X - (advanceWidth * .5F), + origin.Y, + advanceWidth, + metrics.AdvanceHeight * scale.Y); + } + + case GlyphLayoutMode.VerticalRotated: + { + // A rotated glyph advances along the column by its horizontal advance and its + // line box lies across the column. + return new FontRectangle( + origin.X - (emHeight * .5F), + origin.Y, + emHeight, + metrics.AdvanceWidth * scale.X); + } + + default: + { + // The origin is the baseline pen position; the delta-adjusted ascender places + // the cell top exactly where layout places the line-box top above the baseline. + HorizontalMetrics horizontalMetrics = options.Font.FontMetrics.HorizontalMetrics; + float delta = ((horizontalMetrics.LineHeight * scale.Y) - emHeight) * .5F; + float ascender = (horizontalMetrics.Ascender * scale.Y) - delta; + return new FontRectangle( + origin.X, + origin.Y - ascender, + metrics.AdvanceWidth * scale.X, + emHeight); + } + } + } } diff --git a/src/SixLabors.Fonts/TextOptions.cs b/src/SixLabors.Fonts/TextOptions.cs index 64d9b920a..35d5465ff 100644 --- a/src/SixLabors.Fonts/TextOptions.cs +++ b/src/SixLabors.Fonts/TextOptions.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. using System.Numerics; @@ -30,6 +30,7 @@ public class TextOptions public TextOptions(TextOptions options) { this.Font = options.Font; + this.FontWeight = options.FontWeight; this.FallbackFontFamilies = new List(options.FallbackFontFamilies); this.TabWidth = options.TabWidth; this.HintingMode = options.HintingMode; @@ -57,6 +58,7 @@ public TextOptions(TextOptions options) this.FeatureTags = new List(options.FeatureTags); this.TextRuns = new List(options.TextRuns); this.DecorationPositioningMode = options.DecorationPositioningMode; + this.TextDecorationSkipInk = options.TextDecorationSkipInk; } /// @@ -72,6 +74,12 @@ public Font Font } } + /// + /// Gets or sets the font weight, or to use the weight implied by + /// . + /// + public FontWeight? FontWeight { get; set; } + /// /// Gets or sets the collection of fallback font families to use when /// a specific glyph is missing from . @@ -230,6 +238,13 @@ public float LineSpacing /// public DecorationPositioningMode DecorationPositioningMode { get; set; } + /// + /// Gets or sets a value indicating whether underline and overline decorations skip over + /// glyph ink, leaving gaps around descenders and ascenders that cross the decoration. + /// Defaults to . + /// + public TextDecorationSkipInk TextDecorationSkipInk { get; set; } = TextDecorationSkipInk.Auto; + /// /// Gets or sets the color font support options. /// diff --git a/src/SixLabors.Fonts/TextRun.cs b/src/SixLabors.Fonts/TextRun.cs index 963a30db7..21e533f16 100644 --- a/src/SixLabors.Fonts/TextRun.cs +++ b/src/SixLabors.Fonts/TextRun.cs @@ -11,6 +11,8 @@ namespace SixLabors.Fonts; /// public class TextRun { + private Font? resolvedFont; + /// /// Gets or sets the inclusive start index of the first grapheme in this . /// @@ -26,6 +28,12 @@ public class TextRun /// public Font? Font { get; set; } + /// + /// Gets or sets the font weight for this run, or to use the weight + /// supplied by the owning text options. + /// + public FontWeight? FontWeight { get; set; } + /// /// Gets or sets the text attributes applied to this run. /// @@ -44,6 +52,34 @@ public class TextRun /// public TextPlaceholder? Placeholder { get; set; } + /// + /// Gets the font used to shape and render this run. + /// + internal Font ResolvedFont => this.resolvedFont ?? this.Font!; + + /// + /// Gets the effective requested weight for this run. + /// + internal FontWeight? ResolvedFontWeight { get; private set; } + + /// + /// Gets a value indicating whether the requested weight was applied through a variable axis. + /// + internal bool UsesVariableWeight { get; private set; } + + /// + /// Gets the geometry overrides to apply to the specified decoration, or + /// to use the values derived from the font's metrics. + /// + /// + /// The base implementation returns for every decoration. Derived runs can + /// override this to supply values sourced from styling that the font metrics do not describe, such + /// as an explicit stroke width. + /// + /// The decoration to retrieve overrides for. + /// The for the decoration, or . + public virtual TextDecorationOptions? GetDecorationOptions(TextDecorations decoration) => null; + /// /// Returns the slice of the given text representing this . /// @@ -87,6 +123,27 @@ public ReadOnlySpan Slice(ReadOnlySpan text) public override string ToString() => $"[TextRun: Start={this.Start}, End={this.End}, TextAttributes={this.TextAttributes}]"; + /// + /// Resolves the weight used by this run without changing its declared font or weight. + /// + /// The weight supplied by the owning text options. + internal void ResolveFontWeight(FontWeight? defaultWeight) + { + Font font = this.Font!; + this.ResolvedFontWeight = this.FontWeight ?? defaultWeight; + if (!this.ResolvedFontWeight.HasValue && (font.RequestedStyle & FontStyle.Bold) == FontStyle.Bold) + { + this.ResolvedFontWeight = SixLabors.Fonts.FontWeight.Bold; + } + + bool applied = false; + this.resolvedFont = this.ResolvedFontWeight.HasValue + ? font.WithWeight(this.ResolvedFontWeight.Value, out applied) + : font; + + this.UsesVariableWeight = this.ResolvedFontWeight.HasValue && applied; + } + [MethodImpl(MethodImplOptions.NoInlining)] private static void ValidateRange(int start, int end) { diff --git a/src/SixLabors.Fonts/WellKnownIds/KnownNameIds.cs b/src/SixLabors.Fonts/WellKnownIds/KnownNameIds.cs index bbf7cec1d..b577be5fc 100644 --- a/src/SixLabors.Fonts/WellKnownIds/KnownNameIds.cs +++ b/src/SixLabors.Fonts/WellKnownIds/KnownNameIds.cs @@ -118,4 +118,14 @@ public enum KnownNameIds : ushort /// Sample text; This can be the font name, or any other text that the designer thinks is the best sample to display the font in. /// SampleText = 19, + + /// + /// WWS Family name: Used to provide a family grouping for faces that differ only in weight, width, or slope. + /// + WwsFamilyName = 21, + + /// + /// WWS Subfamily name: Used to describe weight, width, or slope within a WWS family grouping. + /// + WwsSubfamilyName = 22, } diff --git a/tests/Browser/FontSynthesis.html b/tests/Browser/FontSynthesis.html new file mode 100644 index 000000000..6c1b257f6 --- /dev/null +++ b/tests/Browser/FontSynthesis.html @@ -0,0 +1,204 @@ + + + + + + Font synthesis browser comparison + + + +
+

Browser font synthesis comparison

+
+ +
+

Open Sans TrueType, synthetic italic, 36pt

+
+
+
Browser
+

Sphinx of black quartz, judge my vow. +Pack my box with five dozen liquor jugs.

+
+
+
SixLabors.Fonts actual output
+ SixLabors.Fonts synthetic italic Open Sans output +
+
+
+ +
+

Plantin CFF, synthetic italic, 36pt

+
+
+
Browser
+

Sphinx of black quartz, judge my vow. +Pack my box with five dozen liquor jugs.

+
+
+
SixLabors.Fonts actual output
+ SixLabors.Fonts synthetic italic Plantin output +
+
+
+ +
+

Open Sans TrueType, synthetic bold, 36pt

+
+
+
Browser
+

Sphinx of black quartz, judge my vow. +Pack my box with five dozen liquor jugs.

+
+
+
SixLabors.Fonts actual output
+ SixLabors.Fonts synthetic bold Open Sans output +
+
+
+ +
+

Plantin CFF, synthetic bold, 36pt

+
+
+
Browser
+

Sphinx of black quartz, judge my vow. +Pack my box with five dozen liquor jugs.

+
+
+
SixLabors.Fonts actual output
+ SixLabors.Fonts synthetic bold Plantin output +
+
+
+ +
+

Twemoji Mozilla COLRv0, synthetic italic, 54pt

+
+
+
Browser
+

😀 ☺️ ❤️ ✌️ ⭐

+
+
+
SixLabors.Fonts actual output
+ SixLabors.Fonts synthetic italic Twemoji output +
+
+
+ +
+

Twemoji Mozilla COLRv0, requested bold, 54pt

+
+
+
Browser
+

😀 ☺️ ❤️ ✌️ ⭐

+
+
+
SixLabors.Fonts actual output
+ SixLabors.Fonts requested-bold Twemoji output +
+
+
+ + diff --git a/tests/Browser/FontWeight.html b/tests/Browser/FontWeight.html new file mode 100644 index 000000000..009e095da --- /dev/null +++ b/tests/Browser/FontWeight.html @@ -0,0 +1,222 @@ + + + + + + Font weight browser comparison + + + +
+

Browser font weight comparison

+
+ +
+

Open Sans static regular face, synthetic weights, 18pt

+
+
+
Browser
+
SixLabors.Fonts actual output
+
+
+ +
+

Roboto Flex variable weight axis, 18pt

+
+
+
Browser
+
SixLabors.Fonts actual output
+
+
+ +
+

Segoe UI system family, 18pt

+
+
+
Browser
+
SixLabors.Fonts actual output
+
+
+ +
+

Arial system family, 18pt

+
+
+
Browser
+
SixLabors.Fonts actual output
+
+
+ +
+

Twemoji Mozilla COLRv0, requested weights, 27pt

+
+
+
Browser
+
SixLabors.Fonts actual output
+
+
+ + + + diff --git a/tests/Images/ReferenceOutput/BreakWordEnsuresSingleCharacterPerLine_1.png b/tests/Images/ReferenceOutput/BreakWordEnsuresSingleCharacterPerLine_1.png index 9be5f2d3a..4b8d68b40 100644 --- a/tests/Images/ReferenceOutput/BreakWordEnsuresSingleCharacterPerLine_1.png +++ b/tests/Images/ReferenceOutput/BreakWordEnsuresSingleCharacterPerLine_1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8426356ff2902da219ff91a08f7e906a572b19bb85171d2259e5e535b647fc4a -size 1311 +oid sha256:cf2d3491230f87bab81443660f473f88aa9f184f4d4aca6798426b9e2b2d8355 +size 1461 diff --git a/tests/Images/ReferenceOutput/CanRenderEmojiFont_With_COLRv1-.png b/tests/Images/ReferenceOutput/CanRenderEmojiFont_With_COLRv1-.png index 29e69c970..00741090a 100644 --- a/tests/Images/ReferenceOutput/CanRenderEmojiFont_With_COLRv1-.png +++ b/tests/Images/ReferenceOutput/CanRenderEmojiFont_With_COLRv1-.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2c88d92de871eaa438d65ea3b6d7e10ced0ccb8ac04d4d6f79529c68818e1ea5 -size 32077 +oid sha256:7f8fc8928e103183654a0597b30e0c771f5d142af23208be51b0f9faf2250b3f +size 32918 diff --git a/tests/Images/ReferenceOutput/CanRenderEmojiFont_With_COLRv1_-G-.png b/tests/Images/ReferenceOutput/CanRenderEmojiFont_With_COLRv1_-G-.png index 8d12b6c08..49ac17477 100644 --- a/tests/Images/ReferenceOutput/CanRenderEmojiFont_With_COLRv1_-G-.png +++ b/tests/Images/ReferenceOutput/CanRenderEmojiFont_With_COLRv1_-G-.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:da6a2bc77408f0819164516c86b3be1a5ca8fadce88c59c555df540d82537dfa -size 10791 +oid sha256:fd66a74de669b1136a6ad05cdb76b178712d77613329ffb829a9b4590407c0e0 +size 11069 diff --git a/tests/Images/ReferenceOutput/CanRenderEmojiFont_With_SVG-.png b/tests/Images/ReferenceOutput/CanRenderEmojiFont_With_SVG-.png index c9df61e44..66c007bd0 100644 --- a/tests/Images/ReferenceOutput/CanRenderEmojiFont_With_SVG-.png +++ b/tests/Images/ReferenceOutput/CanRenderEmojiFont_With_SVG-.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a21b45ef57583c4516f9bfed40bec5d92c4e2be2795866c8bb84c9a9e5b61967 -size 32064 +oid sha256:462cc7567d236a2e17571f0812be601f5ed4880715e7deef976184634dfb1923 +size 32896 diff --git a/tests/Images/ReferenceOutput/CanRenderEmojiFont_With_SVG_-G-.png b/tests/Images/ReferenceOutput/CanRenderEmojiFont_With_SVG_-G-.png index 8d12b6c08..49ac17477 100644 --- a/tests/Images/ReferenceOutput/CanRenderEmojiFont_With_SVG_-G-.png +++ b/tests/Images/ReferenceOutput/CanRenderEmojiFont_With_SVG_-G-.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:da6a2bc77408f0819164516c86b3be1a5ca8fadce88c59c555df540d82537dfa -size 10791 +oid sha256:fd66a74de669b1136a6ad05cdb76b178712d77613329ffb829a9b4590407c0e0 +size 11069 diff --git a/tests/Images/ReferenceOutput/CanRenderEmojiSanityMatrix_With_COLRv1_-full-string-.png b/tests/Images/ReferenceOutput/CanRenderEmojiSanityMatrix_With_COLRv1_-full-string-.png index 20a41191f..265cdb331 100644 --- a/tests/Images/ReferenceOutput/CanRenderEmojiSanityMatrix_With_COLRv1_-full-string-.png +++ b/tests/Images/ReferenceOutput/CanRenderEmojiSanityMatrix_With_COLRv1_-full-string-.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:522cbe7a037065fbab55539265658a27f669940059ab7aab30db0c77e8177c4d -size 728604 +oid sha256:4e2b2f8edec75e83e0db454ae1013ef0959c0cceab52c42e8aa7b202b5e0a647 +size 705036 diff --git a/tests/Images/ReferenceOutput/CanRenderEmojiSanityMatrix_With_SVG_-full-string-.png b/tests/Images/ReferenceOutput/CanRenderEmojiSanityMatrix_With_SVG_-full-string-.png index f2d8cfa63..1fdd68669 100644 --- a/tests/Images/ReferenceOutput/CanRenderEmojiSanityMatrix_With_SVG_-full-string-.png +++ b/tests/Images/ReferenceOutput/CanRenderEmojiSanityMatrix_With_SVG_-full-string-.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7a567ee7c6e94a16bb21462b7cb4bc21d48522ecd30dcbec467c65543f591c24 -size 728456 +oid sha256:897f7f33e268af82c71c9ff0dc3c3edc9619f8550d4fbef152e1173f918573ed +size 704909 diff --git a/tests/Images/ReferenceOutput/CanRenderProblemEmojiTransforms_With_COLRv1_-clown-.png b/tests/Images/ReferenceOutput/CanRenderProblemEmojiTransforms_With_COLRv1_-clown-.png index 591490643..86f93ea7d 100644 --- a/tests/Images/ReferenceOutput/CanRenderProblemEmojiTransforms_With_COLRv1_-clown-.png +++ b/tests/Images/ReferenceOutput/CanRenderProblemEmojiTransforms_With_COLRv1_-clown-.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:45ba2326f7ae9929ed4b6b22c920e5d7bfac56b929d9da4cc1dfad959ad18194 -size 27291 +oid sha256:c2750488d46f73d99090f3a686b5c53096af3d54c299656565111b4df936b315 +size 26810 diff --git a/tests/Images/ReferenceOutput/CanRenderProblemEmojiTransforms_With_COLRv1_-heart-on-fire-.png b/tests/Images/ReferenceOutput/CanRenderProblemEmojiTransforms_With_COLRv1_-heart-on-fire-.png index 3f3345048..579ff7048 100644 --- a/tests/Images/ReferenceOutput/CanRenderProblemEmojiTransforms_With_COLRv1_-heart-on-fire-.png +++ b/tests/Images/ReferenceOutput/CanRenderProblemEmojiTransforms_With_COLRv1_-heart-on-fire-.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5954b5837e5e919df43c63f5a86d492768205ba414cdacab9bd452bfa1dec083 -size 26268 +oid sha256:0aaafc9225451c69f41ec0c940b43d5df1cfef74e9cb2bf659409f5a3f3c95fe +size 25609 diff --git a/tests/Images/ReferenceOutput/CanRenderProblemEmojiTransforms_With_COLRv1_-leg-.png b/tests/Images/ReferenceOutput/CanRenderProblemEmojiTransforms_With_COLRv1_-leg-.png index fccee8a60..ce1dfaa87 100644 --- a/tests/Images/ReferenceOutput/CanRenderProblemEmojiTransforms_With_COLRv1_-leg-.png +++ b/tests/Images/ReferenceOutput/CanRenderProblemEmojiTransforms_With_COLRv1_-leg-.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f58ea1e522831e6e5adfb567d962c2a8247f2fa28aa867f1555e64323cb4121c -size 16025 +oid sha256:450698139c23bf12296c083e80b1733cf036ab06746c001b7206ca9836dbec2c +size 15450 diff --git a/tests/Images/ReferenceOutput/CanRenderProblemEmojiTransforms_With_COLRv1_-mending-heart-.png b/tests/Images/ReferenceOutput/CanRenderProblemEmojiTransforms_With_COLRv1_-mending-heart-.png index 99a0be044..21e6568c1 100644 --- a/tests/Images/ReferenceOutput/CanRenderProblemEmojiTransforms_With_COLRv1_-mending-heart-.png +++ b/tests/Images/ReferenceOutput/CanRenderProblemEmojiTransforms_With_COLRv1_-mending-heart-.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:20a091517ef1977568697f850ad7d10a5f40a88f66a57fd1ae43d5d7617ceafe -size 12092 +oid sha256:51be02dcd28a2604d8ff688928eb118f91197dc0ad1be3127c9606553154c4e8 +size 11546 diff --git a/tests/Images/ReferenceOutput/CanRenderProblemEmojiTransforms_With_COLRv1_-robot-.png b/tests/Images/ReferenceOutput/CanRenderProblemEmojiTransforms_With_COLRv1_-robot-.png index 23544385d..677b965ed 100644 --- a/tests/Images/ReferenceOutput/CanRenderProblemEmojiTransforms_With_COLRv1_-robot-.png +++ b/tests/Images/ReferenceOutput/CanRenderProblemEmojiTransforms_With_COLRv1_-robot-.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3aff34e8b9c7418647d71238b42a40733f697924c1c5c12d5e4e3b1118751d54 -size 10283 +oid sha256:4e3407eadea3cda062781375da19d215bef3b44785e2d1a5d333cae46367d477 +size 10088 diff --git a/tests/Images/ReferenceOutput/CanRenderProblemEmojiTransforms_With_SVG_-clown-.png b/tests/Images/ReferenceOutput/CanRenderProblemEmojiTransforms_With_SVG_-clown-.png index 69ef66b05..4859e02f3 100644 --- a/tests/Images/ReferenceOutput/CanRenderProblemEmojiTransforms_With_SVG_-clown-.png +++ b/tests/Images/ReferenceOutput/CanRenderProblemEmojiTransforms_With_SVG_-clown-.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:37a04f45df2f1dc1958accaa70a374935327264d521ccafb0017f8a4476bc01f -size 27312 +oid sha256:7d15ddb59300d83503d0ca2aa97936084fd8f2f2ea80f18613288f8638e0e2ad +size 26794 diff --git a/tests/Images/ReferenceOutput/CanRenderProblemEmojiTransforms_With_SVG_-heart-on-fire-.png b/tests/Images/ReferenceOutput/CanRenderProblemEmojiTransforms_With_SVG_-heart-on-fire-.png index ad1e173f4..7a7584559 100644 --- a/tests/Images/ReferenceOutput/CanRenderProblemEmojiTransforms_With_SVG_-heart-on-fire-.png +++ b/tests/Images/ReferenceOutput/CanRenderProblemEmojiTransforms_With_SVG_-heart-on-fire-.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:426208ea2f30cf65109239bce93a1cc34aee487e869cdba8b708a0f13c04765c -size 26244 +oid sha256:3598815e2fae866954f70724b6b7b07b017511f9a6c86783a56c33bb6dc03138 +size 25601 diff --git a/tests/Images/ReferenceOutput/CanRenderProblemEmojiTransforms_With_SVG_-leg-.png b/tests/Images/ReferenceOutput/CanRenderProblemEmojiTransforms_With_SVG_-leg-.png index f6ffd7dde..8c5c6f31d 100644 --- a/tests/Images/ReferenceOutput/CanRenderProblemEmojiTransforms_With_SVG_-leg-.png +++ b/tests/Images/ReferenceOutput/CanRenderProblemEmojiTransforms_With_SVG_-leg-.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2549c3c8cb9f60f07ada21e17023e78b859bb1ed5239aa8c10fbe4ff160b13ad -size 16004 +oid sha256:ed696e7b181028871a1f11f0a2be583f186609e9fea85c3d3ba7ffc10a6d1bb5 +size 15348 diff --git a/tests/Images/ReferenceOutput/CanRenderProblemEmojiTransforms_With_SVG_-mending-heart-.png b/tests/Images/ReferenceOutput/CanRenderProblemEmojiTransforms_With_SVG_-mending-heart-.png index 99a0be044..21e6568c1 100644 --- a/tests/Images/ReferenceOutput/CanRenderProblemEmojiTransforms_With_SVG_-mending-heart-.png +++ b/tests/Images/ReferenceOutput/CanRenderProblemEmojiTransforms_With_SVG_-mending-heart-.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:20a091517ef1977568697f850ad7d10a5f40a88f66a57fd1ae43d5d7617ceafe -size 12092 +oid sha256:51be02dcd28a2604d8ff688928eb118f91197dc0ad1be3127c9606553154c4e8 +size 11546 diff --git a/tests/Images/ReferenceOutput/CanRenderProblemEmojiTransforms_With_SVG_-robot-.png b/tests/Images/ReferenceOutput/CanRenderProblemEmojiTransforms_With_SVG_-robot-.png index d68247cfe..29654afdf 100644 --- a/tests/Images/ReferenceOutput/CanRenderProblemEmojiTransforms_With_SVG_-robot-.png +++ b/tests/Images/ReferenceOutput/CanRenderProblemEmojiTransforms_With_SVG_-robot-.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:39f87c5b6c308c086c675b21c7a0497fd6d80ebc6e94de773347fa406d9e50c3 -size 10234 +oid sha256:007dbf5601d633ada8666cce5ca48aa9ebe264a09b3c2b43299903a78752800d +size 10129 diff --git a/tests/Images/ReferenceOutput/CaretPosition_DrawsMovedCarets_-ltr-.png b/tests/Images/ReferenceOutput/CaretPosition_DrawsMovedCarets_-ltr-.png index 319c030d0..ab4b79d4c 100644 --- a/tests/Images/ReferenceOutput/CaretPosition_DrawsMovedCarets_-ltr-.png +++ b/tests/Images/ReferenceOutput/CaretPosition_DrawsMovedCarets_-ltr-.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:052ffe2a519df403fbbc12988b346808df7ecf9d2b0d2b171f6b77af1346c511 -size 6483 +oid sha256:0b051c28b513b6b576fe093910e894f66cf89abf097818d9a6148bb58fc9cb56 +size 6253 diff --git a/tests/Images/ReferenceOutput/CaretPosition_DrawsMovedCarets_-rtl-.png b/tests/Images/ReferenceOutput/CaretPosition_DrawsMovedCarets_-rtl-.png index c0f3102a7..3d12c1167 100644 --- a/tests/Images/ReferenceOutput/CaretPosition_DrawsMovedCarets_-rtl-.png +++ b/tests/Images/ReferenceOutput/CaretPosition_DrawsMovedCarets_-rtl-.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5c95a809edd6b4a796918785e4404ecc320657dbac6d682cbd28c7f22ad2f0c2 -size 6522 +oid sha256:83eb7f221a1218a816ca11df76adba29adff44c0f0d1fe8af1780dc055fe684a +size 6325 diff --git a/tests/Images/ReferenceOutput/CaretPosition_DrawsStartAndEndCarets_-ltr-.png b/tests/Images/ReferenceOutput/CaretPosition_DrawsStartAndEndCarets_-ltr-.png index bffec3317..79299d8cd 100644 --- a/tests/Images/ReferenceOutput/CaretPosition_DrawsStartAndEndCarets_-ltr-.png +++ b/tests/Images/ReferenceOutput/CaretPosition_DrawsStartAndEndCarets_-ltr-.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7a9bdc16959a021a68e057750568bc9505492ca025b1b57faa5688cb8f97b082 -size 5778 +oid sha256:c619d3b3908e4fc1a9df07fc9f88251724424e956adfe7da7636142439fa1c73 +size 4002 diff --git a/tests/Images/ReferenceOutput/CaretPosition_DrawsStartAndEndCarets_-rtl-.png b/tests/Images/ReferenceOutput/CaretPosition_DrawsStartAndEndCarets_-rtl-.png index 0d073d3b7..6ef4fb7e3 100644 --- a/tests/Images/ReferenceOutput/CaretPosition_DrawsStartAndEndCarets_-rtl-.png +++ b/tests/Images/ReferenceOutput/CaretPosition_DrawsStartAndEndCarets_-rtl-.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:07d57fe1cccb680141fb2bba449df6258102083bdcec22a01d0df3f260fe6e97 -size 5739 +oid sha256:df7ecacb00e5766a6d96f703e4536b76e13a8280be5955372c0636174dc7be13 +size 3957 diff --git a/tests/Images/ReferenceOutput/CountLinesWrappingLength_100-3.png b/tests/Images/ReferenceOutput/CountLinesWrappingLength_100-3.png index bfcd65629..1cecd52d1 100644 --- a/tests/Images/ReferenceOutput/CountLinesWrappingLength_100-3.png +++ b/tests/Images/ReferenceOutput/CountLinesWrappingLength_100-3.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e0924dd749b926ef16e5cc9a53f5f629472fa9eccb143bbe8a02a19af3d73d5b -size 2949 +oid sha256:eed5e382fbcac6ab4b00d716385aaedbc6da12b3f27d3a5d2cc2078b8e8ecc74 +size 3126 diff --git a/tests/Images/ReferenceOutput/CountLinesWrappingLength_200-3.png b/tests/Images/ReferenceOutput/CountLinesWrappingLength_200-3.png index 7eb0a21b1..d8a349f53 100644 --- a/tests/Images/ReferenceOutput/CountLinesWrappingLength_200-3.png +++ b/tests/Images/ReferenceOutput/CountLinesWrappingLength_200-3.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5d544f8f5e4074cac719c14a029e45bbd98919a6dca8cd5b641f41131357b1e6 -size 2986 +oid sha256:c47faa1c18e41b40abeda2d4650fb1a07f15ec872e6ef7432311e6c8d538b5b5 +size 3132 diff --git a/tests/Images/ReferenceOutput/CountLinesWrappingLength_25-6.png b/tests/Images/ReferenceOutput/CountLinesWrappingLength_25-6.png index 67c84253e..8aab6b01d 100644 --- a/tests/Images/ReferenceOutput/CountLinesWrappingLength_25-6.png +++ b/tests/Images/ReferenceOutput/CountLinesWrappingLength_25-6.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c69de91d1b52e58ab99b7fbb7cd07223c9064afca316d9b90aefa3182d32d4db -size 3003 +oid sha256:697fafee4482c5f270340df5c92aaa9efc48ba11fdea4aefacc06e1beac90fe8 +size 3157 diff --git a/tests/Images/ReferenceOutput/CountLinesWrappingLength_50-4.png b/tests/Images/ReferenceOutput/CountLinesWrappingLength_50-4.png index d27e8bd8e..b9a078b1b 100644 --- a/tests/Images/ReferenceOutput/CountLinesWrappingLength_50-4.png +++ b/tests/Images/ReferenceOutput/CountLinesWrappingLength_50-4.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cf06ffa0656ffc4cb79053f68fe17846b80b4c27e4e0f4417608a09618670d36 -size 3017 +oid sha256:0e912502f9a4dd09aaa674ddaf7b3906329633b962d59abaab157c38388f0a32 +size 3154 diff --git "a/tests/Images/ReferenceOutput/FontTracking_CorrectlyAddSpacingForComposedCharacterHRef_-\340\244\260\340\245\215\340\244\225\340\244\277-.png" "b/tests/Images/ReferenceOutput/FontTracking_CorrectlyAddSpacingForComposedCharacterHRef_-\340\244\260\340\245\215\340\244\225\340\244\277-.png" index da34da376..fc365a3b2 100644 --- "a/tests/Images/ReferenceOutput/FontTracking_CorrectlyAddSpacingForComposedCharacterHRef_-\340\244\260\340\245\215\340\244\225\340\244\277-.png" +++ "b/tests/Images/ReferenceOutput/FontTracking_CorrectlyAddSpacingForComposedCharacterHRef_-\340\244\260\340\245\215\340\244\225\340\244\277-.png" @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:deebeca3f1f309ba37ee71befbab416da214782327c1ad851926eba3d1c6a3b0 -size 483 +oid sha256:373963b20c7ec55468831d92f3349882fec41411b77ee270bd90171e2a49f140 +size 476 diff --git "a/tests/Images/ReferenceOutput/FontTracking_CorrectlyAddSpacingForComposedCharacterHRef_-\340\244\260\340\245\215\340\244\225\340\244\277\340\244\260\340\245\215\340\244\225\340\244\277-.png" "b/tests/Images/ReferenceOutput/FontTracking_CorrectlyAddSpacingForComposedCharacterHRef_-\340\244\260\340\245\215\340\244\225\340\244\277\340\244\260\340\245\215\340\244\225\340\244\277-.png" index a2b122ed4..f2fbd496c 100644 --- "a/tests/Images/ReferenceOutput/FontTracking_CorrectlyAddSpacingForComposedCharacterHRef_-\340\244\260\340\245\215\340\244\225\340\244\277\340\244\260\340\245\215\340\244\225\340\244\277-.png" +++ "b/tests/Images/ReferenceOutput/FontTracking_CorrectlyAddSpacingForComposedCharacterHRef_-\340\244\260\340\245\215\340\244\225\340\244\277\340\244\260\340\245\215\340\244\225\340\244\277-.png" @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8207ea1f39fd88064dba5aebb771c81a806d4add6c1d4f3a0cab88a08ba49a27 -size 838 +oid sha256:a39e821f4cff0103474915d66298e0e33815ec2cc3f537e052df1178ea65af4f +size 830 diff --git "a/tests/Images/ReferenceOutput/FontTracking_CorrectlyAddSpacingForComposedCharacterHRef_-\340\244\277-.png" "b/tests/Images/ReferenceOutput/FontTracking_CorrectlyAddSpacingForComposedCharacterHRef_-\340\244\277-.png" index 508f1942a..1490f77ec 100644 --- "a/tests/Images/ReferenceOutput/FontTracking_CorrectlyAddSpacingForComposedCharacterHRef_-\340\244\277-.png" +++ "b/tests/Images/ReferenceOutput/FontTracking_CorrectlyAddSpacingForComposedCharacterHRef_-\340\244\277-.png" @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f1cde64636c48b93b2aeeebbf4ce157a3636ad309da3d9334deb0fd52068cd42 -size 355 +oid sha256:68ecfe7ac65e4b4fd07805a12adfe18eda760ad26074c9099f1dc12f11785d07 +size 349 diff --git "a/tests/Images/ReferenceOutput/FontTracking_CorrectlyAddSpacingForComposedCharacterHRef_-\340\244\277a-.png" "b/tests/Images/ReferenceOutput/FontTracking_CorrectlyAddSpacingForComposedCharacterHRef_-\340\244\277a-.png" index 7862ff7f9..7a60bda07 100644 --- "a/tests/Images/ReferenceOutput/FontTracking_CorrectlyAddSpacingForComposedCharacterHRef_-\340\244\277a-.png" +++ "b/tests/Images/ReferenceOutput/FontTracking_CorrectlyAddSpacingForComposedCharacterHRef_-\340\244\277a-.png" @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4592f63fe5e39e8d8ba298855eef1ae2356220d0ddca50ae0a5d10fbdbcf9402 -size 400 +oid sha256:befbcad3b25cbf628d8a2ee74cf1159e6bc8e99ce009d4775d321951dbe6139d +size 394 diff --git "a/tests/Images/ReferenceOutput/FontTracking_CorrectlyAddSpacingForComposedCharacterVMRef_-\340\244\260\340\245\215\340\244\225\340\244\277-.png" "b/tests/Images/ReferenceOutput/FontTracking_CorrectlyAddSpacingForComposedCharacterVMRef_-\340\244\260\340\245\215\340\244\225\340\244\277-.png" index 5f983034b..7719947a6 100644 --- "a/tests/Images/ReferenceOutput/FontTracking_CorrectlyAddSpacingForComposedCharacterVMRef_-\340\244\260\340\245\215\340\244\225\340\244\277-.png" +++ "b/tests/Images/ReferenceOutput/FontTracking_CorrectlyAddSpacingForComposedCharacterVMRef_-\340\244\260\340\245\215\340\244\225\340\244\277-.png" @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5953d7667d33fb757bedded9663311fd0a8cd0c4d118e8b95351730bcad213a8 -size 478 +oid sha256:abcd2c70c9fe1406565678f971c37821865cd9a1a2f5dbc34c492222b5a5699a +size 472 diff --git "a/tests/Images/ReferenceOutput/FontTracking_CorrectlyAddSpacingForComposedCharacterVMRef_-\340\244\260\340\245\215\340\244\225\340\244\277\340\244\260\340\245\215\340\244\225\340\244\277-.png" "b/tests/Images/ReferenceOutput/FontTracking_CorrectlyAddSpacingForComposedCharacterVMRef_-\340\244\260\340\245\215\340\244\225\340\244\277\340\244\260\340\245\215\340\244\225\340\244\277-.png" index a733b3aac..085c2afa4 100644 --- "a/tests/Images/ReferenceOutput/FontTracking_CorrectlyAddSpacingForComposedCharacterVMRef_-\340\244\260\340\245\215\340\244\225\340\244\277\340\244\260\340\245\215\340\244\225\340\244\277-.png" +++ "b/tests/Images/ReferenceOutput/FontTracking_CorrectlyAddSpacingForComposedCharacterVMRef_-\340\244\260\340\245\215\340\244\225\340\244\277\340\244\260\340\245\215\340\244\225\340\244\277-.png" @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5dd8160653f786b207e892ac1bf5a66c12626670b671b761781131fa991ec30c -size 842 +oid sha256:946f9f51cb2f8b9ec725edb953f7c49e87fca29f72855b7662babc9b228025a0 +size 829 diff --git "a/tests/Images/ReferenceOutput/FontTracking_CorrectlyAddSpacingForComposedCharacterVMRef_-\340\244\277-.png" "b/tests/Images/ReferenceOutput/FontTracking_CorrectlyAddSpacingForComposedCharacterVMRef_-\340\244\277-.png" index 49973b4fe..3e1e70b60 100644 --- "a/tests/Images/ReferenceOutput/FontTracking_CorrectlyAddSpacingForComposedCharacterVMRef_-\340\244\277-.png" +++ "b/tests/Images/ReferenceOutput/FontTracking_CorrectlyAddSpacingForComposedCharacterVMRef_-\340\244\277-.png" @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3ef301a9af536eb2e0657174eb2d0395e81798bdfc1dd280a30d01445a464ecf -size 358 +oid sha256:3037f5b6b593fe0de5bf701fb3d439dcf55b91d1bc0eaf1e6909c8061a8d2534 +size 351 diff --git "a/tests/Images/ReferenceOutput/FontTracking_CorrectlyAddSpacingForComposedCharacterVMRef_-\340\244\277a-.png" "b/tests/Images/ReferenceOutput/FontTracking_CorrectlyAddSpacingForComposedCharacterVMRef_-\340\244\277a-.png" index 67fd8be3b..99d9ce1a7 100644 --- "a/tests/Images/ReferenceOutput/FontTracking_CorrectlyAddSpacingForComposedCharacterVMRef_-\340\244\277a-.png" +++ "b/tests/Images/ReferenceOutput/FontTracking_CorrectlyAddSpacingForComposedCharacterVMRef_-\340\244\277a-.png" @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8905147d38fbfc5aea28a183547e6aa4b053cd3d816f497b1c9f8f261a9c165a -size 408 +oid sha256:a92b1b666ecb0d8962bd341efdca90108d429c66d90a99dda5c19bc905835408 +size 403 diff --git "a/tests/Images/ReferenceOutput/FontTracking_CorrectlyAddSpacingForComposedCharacterVRef_-\340\244\260\340\245\215\340\244\225\340\244\277-.png" "b/tests/Images/ReferenceOutput/FontTracking_CorrectlyAddSpacingForComposedCharacterVRef_-\340\244\260\340\245\215\340\244\225\340\244\277-.png" index d6e7326e1..7fc8f476d 100644 --- "a/tests/Images/ReferenceOutput/FontTracking_CorrectlyAddSpacingForComposedCharacterVRef_-\340\244\260\340\245\215\340\244\225\340\244\277-.png" +++ "b/tests/Images/ReferenceOutput/FontTracking_CorrectlyAddSpacingForComposedCharacterVRef_-\340\244\260\340\245\215\340\244\225\340\244\277-.png" @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:562dbccabfd0b25e2bbc6ff7f0cd880025fae78c4c6c0a085abcf6fa0c35185e -size 504 +oid sha256:65420c6443c09be787007d55a93f76f8b04e9b2182d9a2a152da8af659e7a7db +size 500 diff --git "a/tests/Images/ReferenceOutput/FontTracking_CorrectlyAddSpacingForComposedCharacterVRef_-\340\244\260\340\245\215\340\244\225\340\244\277\340\244\260\340\245\215\340\244\225\340\244\277-.png" "b/tests/Images/ReferenceOutput/FontTracking_CorrectlyAddSpacingForComposedCharacterVRef_-\340\244\260\340\245\215\340\244\225\340\244\277\340\244\260\340\245\215\340\244\225\340\244\277-.png" index 3dd6f27fc..8e16146c1 100644 --- "a/tests/Images/ReferenceOutput/FontTracking_CorrectlyAddSpacingForComposedCharacterVRef_-\340\244\260\340\245\215\340\244\225\340\244\277\340\244\260\340\245\215\340\244\225\340\244\277-.png" +++ "b/tests/Images/ReferenceOutput/FontTracking_CorrectlyAddSpacingForComposedCharacterVRef_-\340\244\260\340\245\215\340\244\225\340\244\277\340\244\260\340\245\215\340\244\225\340\244\277-.png" @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f9fb30702add87809737da871de03ac997e4102a5682aef2fde198c7c4943ecc -size 883 +oid sha256:eb2ee8be72610c10118191cb037306703868352f09ac7b7ff8a9ab8d44128819 +size 866 diff --git "a/tests/Images/ReferenceOutput/FontTracking_CorrectlyAddSpacingForComposedCharacterVRef_-\340\244\277-.png" "b/tests/Images/ReferenceOutput/FontTracking_CorrectlyAddSpacingForComposedCharacterVRef_-\340\244\277-.png" index 37047d3a8..711cf89be 100644 --- "a/tests/Images/ReferenceOutput/FontTracking_CorrectlyAddSpacingForComposedCharacterVRef_-\340\244\277-.png" +++ "b/tests/Images/ReferenceOutput/FontTracking_CorrectlyAddSpacingForComposedCharacterVRef_-\340\244\277-.png" @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8325afcb70c83f4bc351bf2d494b9afb811436801289b696b56dbef9a99ee09e -size 354 +oid sha256:b3939810c970e0845f2bc04e95d54a88cff5d18d8b02cdc62dba8e12bfe08c78 +size 350 diff --git "a/tests/Images/ReferenceOutput/FontTracking_CorrectlyAddSpacingForComposedCharacterVRef_-\340\244\277a-.png" "b/tests/Images/ReferenceOutput/FontTracking_CorrectlyAddSpacingForComposedCharacterVRef_-\340\244\277a-.png" index 766c8710c..5ae02c536 100644 --- "a/tests/Images/ReferenceOutput/FontTracking_CorrectlyAddSpacingForComposedCharacterVRef_-\340\244\277a-.png" +++ "b/tests/Images/ReferenceOutput/FontTracking_CorrectlyAddSpacingForComposedCharacterVRef_-\340\244\277a-.png" @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:785afa44e73977d8fc4c0cbb7f809d875b504393e1db613ccedeb36c18601523 -size 402 +oid sha256:78569328e764acbd28b6e56b88c055c3ff3ab1f1fada466473e1d6f174083d5a +size 396 diff --git a/tests/Images/ReferenceOutput/GraphemeMetrics_GetSelectionBounds_DrawsBidiDragSelection-.png b/tests/Images/ReferenceOutput/GraphemeMetrics_GetSelectionBounds_DrawsBidiDragSelection-.png index 9ca811ff7..d67f557bb 100644 --- a/tests/Images/ReferenceOutput/GraphemeMetrics_GetSelectionBounds_DrawsBidiDragSelection-.png +++ b/tests/Images/ReferenceOutput/GraphemeMetrics_GetSelectionBounds_DrawsBidiDragSelection-.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:77244e6fc510a10c45ab876f3144eb9ee6afdd7341d78fb6072c69a55e4205c8 -size 2877 +oid sha256:f7f41c1da473f90208b8ab2a161284f7e1cd9cde248cc69dfc84f79c954400d7 +size 2795 diff --git a/tests/Images/ReferenceOutput/GraphemeMetrics_GetSelectionBounds_DrawsGraphemeSelections_HorizontalBottomTop.png b/tests/Images/ReferenceOutput/GraphemeMetrics_GetSelectionBounds_DrawsGraphemeSelections_HorizontalBottomTop.png index e6f33dc09..75ae99e36 100644 --- a/tests/Images/ReferenceOutput/GraphemeMetrics_GetSelectionBounds_DrawsGraphemeSelections_HorizontalBottomTop.png +++ b/tests/Images/ReferenceOutput/GraphemeMetrics_GetSelectionBounds_DrawsGraphemeSelections_HorizontalBottomTop.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b833082801af2163d40ed722c57a16f125b9ff407a8ab9c9a15de1e44a3508ce -size 10774 +oid sha256:cf12d0452b005a56399bc03aa68a679656187e9aa958ca94c65e98fd6703ac6f +size 10624 diff --git a/tests/Images/ReferenceOutput/GraphemeMetrics_GetSelectionBounds_DrawsGraphemeSelections_HorizontalTopBottom.png b/tests/Images/ReferenceOutput/GraphemeMetrics_GetSelectionBounds_DrawsGraphemeSelections_HorizontalTopBottom.png index 2659f9611..2566aaaac 100644 --- a/tests/Images/ReferenceOutput/GraphemeMetrics_GetSelectionBounds_DrawsGraphemeSelections_HorizontalTopBottom.png +++ b/tests/Images/ReferenceOutput/GraphemeMetrics_GetSelectionBounds_DrawsGraphemeSelections_HorizontalTopBottom.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:156a15b0758982583d8fa029ff087a53af4942bd0348d69127a42dd58e740d59 -size 10924 +oid sha256:2e8ab0d2aaa81507895e31cc951178e06396dc1dff94ffcee8907f1bec93675f +size 10764 diff --git a/tests/Images/ReferenceOutput/GraphemeMetrics_GetSelectionBounds_DrawsGraphemeSelections_VerticalLeftRight.png b/tests/Images/ReferenceOutput/GraphemeMetrics_GetSelectionBounds_DrawsGraphemeSelections_VerticalLeftRight.png index 447f73635..cda45ac05 100644 --- a/tests/Images/ReferenceOutput/GraphemeMetrics_GetSelectionBounds_DrawsGraphemeSelections_VerticalLeftRight.png +++ b/tests/Images/ReferenceOutput/GraphemeMetrics_GetSelectionBounds_DrawsGraphemeSelections_VerticalLeftRight.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:29d4c2e44dc8aa6328d3b58997027d5e40fefa48e89793ad4deb7d68bf3c74a8 -size 16094 +oid sha256:6a36458de1afddfa3000f302c5148f76903b83efbeaa30d48d303eea560dd98b +size 15593 diff --git a/tests/Images/ReferenceOutput/GraphemeMetrics_GetSelectionBounds_DrawsGraphemeSelections_VerticalMixedLeftRight.png b/tests/Images/ReferenceOutput/GraphemeMetrics_GetSelectionBounds_DrawsGraphemeSelections_VerticalMixedLeftRight.png index b85a98cac..3e729e3eb 100644 --- a/tests/Images/ReferenceOutput/GraphemeMetrics_GetSelectionBounds_DrawsGraphemeSelections_VerticalMixedLeftRight.png +++ b/tests/Images/ReferenceOutput/GraphemeMetrics_GetSelectionBounds_DrawsGraphemeSelections_VerticalMixedLeftRight.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:341ddb22bdd1ee450c9a6e0eb41bde3eeafe03ce7a6ff503f3654c86cfa2ccbe -size 12024 +oid sha256:ef05b7f0f8980bb45f731a490f6d05459f9fc03d78b85dd67d59144ff5e49a48 +size 12101 diff --git a/tests/Images/ReferenceOutput/GraphemeMetrics_GetSelectionBounds_DrawsGraphemeSelections_VerticalMixedRightLeft.png b/tests/Images/ReferenceOutput/GraphemeMetrics_GetSelectionBounds_DrawsGraphemeSelections_VerticalMixedRightLeft.png index 7abb5ea9b..ffa6ad61e 100644 --- a/tests/Images/ReferenceOutput/GraphemeMetrics_GetSelectionBounds_DrawsGraphemeSelections_VerticalMixedRightLeft.png +++ b/tests/Images/ReferenceOutput/GraphemeMetrics_GetSelectionBounds_DrawsGraphemeSelections_VerticalMixedRightLeft.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1f96a20afcba1ad3145698c83ca9290e9cd93f0fe65ae3c33e65a1e043a87bf9 -size 11948 +oid sha256:e50cea59d264c07217548b532161f90205fe8a938fc2f1ada1e2f65c24850bf8 +size 12020 diff --git a/tests/Images/ReferenceOutput/GraphemeMetrics_GetSelectionBounds_DrawsGraphemeSelections_VerticalRightLeft.png b/tests/Images/ReferenceOutput/GraphemeMetrics_GetSelectionBounds_DrawsGraphemeSelections_VerticalRightLeft.png index 96010f88d..62e753162 100644 --- a/tests/Images/ReferenceOutput/GraphemeMetrics_GetSelectionBounds_DrawsGraphemeSelections_VerticalRightLeft.png +++ b/tests/Images/ReferenceOutput/GraphemeMetrics_GetSelectionBounds_DrawsGraphemeSelections_VerticalRightLeft.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f7d72a658c9e4c24c9e8799745dcf831c511d32878b5cb699947251dce622a9c -size 16011 +oid sha256:3e4012619a36bab6981dca4fe60c41ff2edae0f0c766ce768778e80e3d148d39 +size 15669 diff --git a/tests/Images/ReferenceOutput/GraphemeMetrics_GetSelectionBounds_DrawsSelectionWithBlankLine-.png b/tests/Images/ReferenceOutput/GraphemeMetrics_GetSelectionBounds_DrawsSelectionWithBlankLine-.png index 0fc2ea822..8e0f53828 100644 --- a/tests/Images/ReferenceOutput/GraphemeMetrics_GetSelectionBounds_DrawsSelectionWithBlankLine-.png +++ b/tests/Images/ReferenceOutput/GraphemeMetrics_GetSelectionBounds_DrawsSelectionWithBlankLine-.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b3bd048e157a58975b8db7157383c4423befd8635c6aea38c397a9ae73c3b041 -size 6540 +oid sha256:e7d70189d4c7fb30ac10440783d7c11b7a824c3f3c1b548485cdcfe5b44aff9b +size 6404 diff --git a/tests/Images/ReferenceOutput/Issue_444_A_860.png b/tests/Images/ReferenceOutput/Issue_444_A_860.png index c5f4b38df..9b4f04f38 100644 --- a/tests/Images/ReferenceOutput/Issue_444_A_860.png +++ b/tests/Images/ReferenceOutput/Issue_444_A_860.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e82938e6436a9b1a6e1fd38f7c5f2e09033b7b1aab48b5d4289a5e5c20ffc699 -size 35048 +oid sha256:870a7d87c5c82c92a8e8b2ce0a965eb1d492a0123b6fb5b08de71b3ac6605f71 +size 20120 diff --git a/tests/Images/ReferenceOutput/Issue_444_B_860.png b/tests/Images/ReferenceOutput/Issue_444_B_860.png index 50bd7a697..de1faf6d9 100644 --- a/tests/Images/ReferenceOutput/Issue_444_B_860.png +++ b/tests/Images/ReferenceOutput/Issue_444_B_860.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9322aea363e287a3387209e86be3612c26d8715e949c98eb5980dd7800470519 -size 34954 +oid sha256:4b06484ef44e91e76d0d41d22c37c135da741bb048f8648bda06fd759eef1630 +size 20101 diff --git a/tests/Images/ReferenceOutput/Issue_444_C_860.png b/tests/Images/ReferenceOutput/Issue_444_C_860.png index 6611ed349..316d133b6 100644 --- a/tests/Images/ReferenceOutput/Issue_444_C_860.png +++ b/tests/Images/ReferenceOutput/Issue_444_C_860.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7079c0f0ceef9b77727b3d8ce8d79636b20d79a2a1d8d39dcb760808711a9596 -size 34843 +oid sha256:c82adca20fd46304a5be52f4ede7af1bc6bd26dea44bfbcc8f74e9f7897640da +size 20947 diff --git a/tests/Images/ReferenceOutput/Issue_444_D_860.png b/tests/Images/ReferenceOutput/Issue_444_D_860.png index 6611ed349..316d133b6 100644 --- a/tests/Images/ReferenceOutput/Issue_444_D_860.png +++ b/tests/Images/ReferenceOutput/Issue_444_D_860.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7079c0f0ceef9b77727b3d8ce8d79636b20d79a2a1d8d39dcb760808711a9596 -size 34843 +oid sha256:c82adca20fd46304a5be52f4ede7af1bc6bd26dea44bfbcc8f74e9f7897640da +size 20947 diff --git a/tests/Images/ReferenceOutput/Issue_444_E_860.png b/tests/Images/ReferenceOutput/Issue_444_E_860.png index 373f7ab10..8535e3222 100644 --- a/tests/Images/ReferenceOutput/Issue_444_E_860.png +++ b/tests/Images/ReferenceOutput/Issue_444_E_860.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b07b57454611fbca5b9557d20354db0c220bb7bd0771e5849892a33b5008d83e -size 34860 +oid sha256:2c2884bbfb28324acf6532e08f72be2bbef319c20580dc1f277eeb4793f10c74 +size 20951 diff --git a/tests/Images/ReferenceOutput/Issue_446_A_860.png b/tests/Images/ReferenceOutput/Issue_446_A_860.png index b3f4c0063..fc2bdac45 100644 --- a/tests/Images/ReferenceOutput/Issue_446_A_860.png +++ b/tests/Images/ReferenceOutput/Issue_446_A_860.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:696cd129751f1e080f46f37875cb9d9c7284f1a1f3ca706f96728575257e497c -size 31634 +oid sha256:e37ee3662d84c40a231b9b6ec8d742329532fb93a62d29f9b882b01a23246083 +size 18849 diff --git a/tests/Images/ReferenceOutput/Issue_446_B_860.png b/tests/Images/ReferenceOutput/Issue_446_B_860.png index 925f05e5e..2dcaab3db 100644 --- a/tests/Images/ReferenceOutput/Issue_446_B_860.png +++ b/tests/Images/ReferenceOutput/Issue_446_B_860.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d3df83313f3bb1a1f174a20b3a8b40dae9397049252141efbe13c7da588033e8 -size 32186 +oid sha256:896f7ac3790172e6b269cc47a7c4249c8a98c370d8778273fd30f7fb95c485f3 +size 19195 diff --git a/tests/Images/ReferenceOutput/Issue_448_150.png b/tests/Images/ReferenceOutput/Issue_448_150.png index 5b8e14333..52e91a150 100644 --- a/tests/Images/ReferenceOutput/Issue_448_150.png +++ b/tests/Images/ReferenceOutput/Issue_448_150.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b0d692034b7c7745d129eb1e064f51da1f7cbfc7b7ac7e49fc425eaedb8ce31a -size 2601 +oid sha256:60173bcad00a5606c566b1eddf9d2462583177411a824a9fd03c7ae383988e02 +size 2495 diff --git a/tests/Images/ReferenceOutput/Issue_450_960.png b/tests/Images/ReferenceOutput/Issue_450_960.png index 2605deceb..61e186b6f 100644 --- a/tests/Images/ReferenceOutput/Issue_450_960.png +++ b/tests/Images/ReferenceOutput/Issue_450_960.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:95791d9a1bdf1dca06406af71343b4eada1fa0266f6f425f45efd6cd139acefc -size 20251 +oid sha256:595f818e13fee59a0b12d985ca58c1ff8c02d37def23d21f6c959a72e924344c +size 12152 diff --git a/tests/Images/ReferenceOutput/Issue_451_A-.png b/tests/Images/ReferenceOutput/Issue_451_A-.png index 78a51d2d4..ccdeb644e 100644 --- a/tests/Images/ReferenceOutput/Issue_451_A-.png +++ b/tests/Images/ReferenceOutput/Issue_451_A-.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c21cb84bd41131527eeb3b17b256fbc37dfd484425d608cf1b1cef4f6509b7d0 -size 2152 +oid sha256:e553a01be0516f7807e6e1b1ce70c8dfa801e16130fe38eee877fe7676686cd5 +size 2127 diff --git a/tests/Images/ReferenceOutput/Issue_451_B-.png b/tests/Images/ReferenceOutput/Issue_451_B-.png index d197b7d66..ff9264b3f 100644 --- a/tests/Images/ReferenceOutput/Issue_451_B-.png +++ b/tests/Images/ReferenceOutput/Issue_451_B-.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6f19cb7f99b251a81259def23cd82170b0d334fb855c72a919fc6ce2252f1519 -size 1807 +oid sha256:b855bf844a049579c6bae4b7b8e67b89c4c4348a82015e13c12921c44e5cadde +size 1788 diff --git a/tests/Images/ReferenceOutput/Issue_451_C-.png b/tests/Images/ReferenceOutput/Issue_451_C-.png index d1323f4e1..431ab4b23 100644 --- a/tests/Images/ReferenceOutput/Issue_451_C-.png +++ b/tests/Images/ReferenceOutput/Issue_451_C-.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1d742e5f9c5297d41593db6d423ebfa765fcdcaddf3e2bf4ee37934ff0eefe31 -size 22901 +oid sha256:3c831a1ca40d5a55688737b7be3267d6d18a9eee3b50d5a556319e910dea3533 +size 22427 diff --git a/tests/Images/ReferenceOutput/LineLayoutEnumerator_DrawsManualFlowAroundCircle-.png b/tests/Images/ReferenceOutput/LineLayoutEnumerator_DrawsManualFlowAroundCircle-.png index 2a03011aa..27287d169 100644 --- a/tests/Images/ReferenceOutput/LineLayoutEnumerator_DrawsManualFlowAroundCircle-.png +++ b/tests/Images/ReferenceOutput/LineLayoutEnumerator_DrawsManualFlowAroundCircle-.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1fa1f89bf6f61f3bcd0e396897a3c1740537c4527a9394187c9a49436ac6b3d5 -size 40297 +oid sha256:545e68cf0a07d3465e27a50f4f666d60576172e6b7456e9f85b7ac84b6b8f4b2 +size 41587 diff --git a/tests/Images/ReferenceOutput/LineMetrics_StartAndExtent_DrawsLineBoxes_HorizontalBottomTop.png b/tests/Images/ReferenceOutput/LineMetrics_StartAndExtent_DrawsLineBoxes_HorizontalBottomTop.png index e09ba6438..65e15b49c 100644 --- a/tests/Images/ReferenceOutput/LineMetrics_StartAndExtent_DrawsLineBoxes_HorizontalBottomTop.png +++ b/tests/Images/ReferenceOutput/LineMetrics_StartAndExtent_DrawsLineBoxes_HorizontalBottomTop.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d9cac10eca5495dc20d48f263f220181bebb74a6a54771f50baae9f584fe4650 -size 10606 +oid sha256:f14b097d9d20505ce8f1c8ecf363a5662a91b362cbcf29090a04522ce901bafe +size 10427 diff --git a/tests/Images/ReferenceOutput/LineMetrics_StartAndExtent_DrawsLineBoxes_HorizontalTopBottom.png b/tests/Images/ReferenceOutput/LineMetrics_StartAndExtent_DrawsLineBoxes_HorizontalTopBottom.png index b4abf934b..0be3d2db6 100644 --- a/tests/Images/ReferenceOutput/LineMetrics_StartAndExtent_DrawsLineBoxes_HorizontalTopBottom.png +++ b/tests/Images/ReferenceOutput/LineMetrics_StartAndExtent_DrawsLineBoxes_HorizontalTopBottom.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3f52f53bc9e0c001a4ae6401b50d41fd565c260f53a7fc2e1fe158c49541a806 -size 10741 +oid sha256:34977edfabbcd74afd4d42bbacfc3308474c05c5c58a8dd5464d8257ca94e1ce +size 10403 diff --git a/tests/Images/ReferenceOutput/LineMetrics_StartAndExtent_DrawsLineBoxes_VerticalLeftRight.png b/tests/Images/ReferenceOutput/LineMetrics_StartAndExtent_DrawsLineBoxes_VerticalLeftRight.png index aaf153ac6..9ef9e4d95 100644 --- a/tests/Images/ReferenceOutput/LineMetrics_StartAndExtent_DrawsLineBoxes_VerticalLeftRight.png +++ b/tests/Images/ReferenceOutput/LineMetrics_StartAndExtent_DrawsLineBoxes_VerticalLeftRight.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f8eff1d0af82a324e0691e6230dd6ba3691338a5acd913a163c1be04d2692fcc -size 16105 +oid sha256:1eb0c00fe7ef4e6d2a4ac087264983ebd12972b58cae89c6091ba521c8ec8ef2 +size 16016 diff --git a/tests/Images/ReferenceOutput/LineMetrics_StartAndExtent_DrawsLineBoxes_VerticalMixedLeftRight.png b/tests/Images/ReferenceOutput/LineMetrics_StartAndExtent_DrawsLineBoxes_VerticalMixedLeftRight.png index c655fa51e..c20d08f56 100644 --- a/tests/Images/ReferenceOutput/LineMetrics_StartAndExtent_DrawsLineBoxes_VerticalMixedLeftRight.png +++ b/tests/Images/ReferenceOutput/LineMetrics_StartAndExtent_DrawsLineBoxes_VerticalMixedLeftRight.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8543ca9377060d8b4292ae0b098019acdd42201dbbed69d31c854d6b4da043be -size 12114 +oid sha256:6155b1b17e7e18d29cb4481f83d63893e128204d050312a1779eea7d9682ac66 +size 11881 diff --git a/tests/Images/ReferenceOutput/LineMetrics_StartAndExtent_DrawsLineBoxes_VerticalMixedRightLeft.png b/tests/Images/ReferenceOutput/LineMetrics_StartAndExtent_DrawsLineBoxes_VerticalMixedRightLeft.png index a16e6c999..d85463d8f 100644 --- a/tests/Images/ReferenceOutput/LineMetrics_StartAndExtent_DrawsLineBoxes_VerticalMixedRightLeft.png +++ b/tests/Images/ReferenceOutput/LineMetrics_StartAndExtent_DrawsLineBoxes_VerticalMixedRightLeft.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9a07a89b3f29641b63ad41126e82c1e479b51f70038be5e5752210a8e9b5a36d -size 12035 +oid sha256:35507cf0aa5d6a0e59da2b4351209cbbd3b1338d8b9ea325527d2ac1a4159cde +size 11830 diff --git a/tests/Images/ReferenceOutput/LineMetrics_StartAndExtent_DrawsLineBoxes_VerticalRightLeft.png b/tests/Images/ReferenceOutput/LineMetrics_StartAndExtent_DrawsLineBoxes_VerticalRightLeft.png index b83739078..99bfe3945 100644 --- a/tests/Images/ReferenceOutput/LineMetrics_StartAndExtent_DrawsLineBoxes_VerticalRightLeft.png +++ b/tests/Images/ReferenceOutput/LineMetrics_StartAndExtent_DrawsLineBoxes_VerticalRightLeft.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:132d0e2aba30fd7bcd83a00cb81cc31e1b18db9f0cf4a9a7e3fb8c0bc2c5db4c -size 15779 +oid sha256:823d43de4b9f1fe4af552218e938c2551f3da555051e612c53fda64a3685f7a5 +size 15858 diff --git a/tests/Images/ReferenceOutput/LineWrappingWithExplicitNewLine_800.png b/tests/Images/ReferenceOutput/LineWrappingWithExplicitNewLine_800.png index 25915608f..5956ac6f1 100644 --- a/tests/Images/ReferenceOutput/LineWrappingWithExplicitNewLine_800.png +++ b/tests/Images/ReferenceOutput/LineWrappingWithExplicitNewLine_800.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1bbdc64401aa455e1f71a9214152e6c644f647e2c08448d5f356def1ca0b667f -size 17097 +oid sha256:badb8849d647600d1fb0d1f1a27978c3d63968c71c46404ec09546680382d5b6 +size 10935 diff --git a/tests/Images/ReferenceOutput/LineWrappingWithImplicitNewLine_800.png b/tests/Images/ReferenceOutput/LineWrappingWithImplicitNewLine_800.png index 8e5be4cca..8e5cfab3e 100644 --- a/tests/Images/ReferenceOutput/LineWrappingWithImplicitNewLine_800.png +++ b/tests/Images/ReferenceOutput/LineWrappingWithImplicitNewLine_800.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:aa3756c7ad8d3286ca2c0f2f47774e115a1e28cec2a7f6ff41d2079264132fd4 -size 16390 +oid sha256:4106333c51aecb85c6233fc5e8ef8dae4dc380de6b2a2bb66b71b19f7ed6dbf1 +size 10647 diff --git a/tests/Images/ReferenceOutput/MeasureTextWordBreakMatchesMDN_238-_layoutMode_HorizontalBottomTop-wordBreaking_BreakAll_.png b/tests/Images/ReferenceOutput/MeasureTextWordBreakMatchesMDN_238-_layoutMode_HorizontalBottomTop-wordBreaking_BreakAll_.png index 0736e475d..1f45a3e76 100644 --- a/tests/Images/ReferenceOutput/MeasureTextWordBreakMatchesMDN_238-_layoutMode_HorizontalBottomTop-wordBreaking_BreakAll_.png +++ b/tests/Images/ReferenceOutput/MeasureTextWordBreakMatchesMDN_238-_layoutMode_HorizontalBottomTop-wordBreaking_BreakAll_.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:483c614e880637e5c2f402ce86d8356b0c37828488382bdb6021561545e0c901 -size 13511 +oid sha256:cf2f2a554a256c4ff4f294e122a14ffad107338885e48240d291d7216bb683ae +size 10917 diff --git a/tests/Images/ReferenceOutput/MeasureTextWordBreakMatchesMDN_238-_layoutMode_HorizontalBottomTop-wordBreaking_BreakWord_.png b/tests/Images/ReferenceOutput/MeasureTextWordBreakMatchesMDN_238-_layoutMode_HorizontalBottomTop-wordBreaking_BreakWord_.png index b263c282c..b0bad1e1a 100644 --- a/tests/Images/ReferenceOutput/MeasureTextWordBreakMatchesMDN_238-_layoutMode_HorizontalBottomTop-wordBreaking_BreakWord_.png +++ b/tests/Images/ReferenceOutput/MeasureTextWordBreakMatchesMDN_238-_layoutMode_HorizontalBottomTop-wordBreaking_BreakWord_.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:67e30f71f6f724924c31ba54290e239beae2bbd45bc6610d168374837833a4d2 -size 13846 +oid sha256:ff6276fa27b0b1864e12057f8359744030b6175fedfa8fce9f41ead901be36b7 +size 10870 diff --git a/tests/Images/ReferenceOutput/MeasureTextWordBreakMatchesMDN_238-_layoutMode_HorizontalBottomTop-wordBreaking_KeepAll_.png b/tests/Images/ReferenceOutput/MeasureTextWordBreakMatchesMDN_238-_layoutMode_HorizontalBottomTop-wordBreaking_KeepAll_.png index 6b368d93e..0e548b9b6 100644 --- a/tests/Images/ReferenceOutput/MeasureTextWordBreakMatchesMDN_238-_layoutMode_HorizontalBottomTop-wordBreaking_KeepAll_.png +++ b/tests/Images/ReferenceOutput/MeasureTextWordBreakMatchesMDN_238-_layoutMode_HorizontalBottomTop-wordBreaking_KeepAll_.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5a3d4ee89798cdb6110a57c08b2e6757f3fe29cc31a3847352a0cc6671d42f3e -size 14328 +oid sha256:fd9b08cf6b05e35f1f6eb4e7050e77ba7313a459cec39958017f0bf6d2fe3721 +size 10908 diff --git a/tests/Images/ReferenceOutput/MeasureTextWordBreakMatchesMDN_238-_layoutMode_HorizontalBottomTop-wordBreaking_Standard_.png b/tests/Images/ReferenceOutput/MeasureTextWordBreakMatchesMDN_238-_layoutMode_HorizontalBottomTop-wordBreaking_Standard_.png index 60a5f6f82..e98bf1bb5 100644 --- a/tests/Images/ReferenceOutput/MeasureTextWordBreakMatchesMDN_238-_layoutMode_HorizontalBottomTop-wordBreaking_Standard_.png +++ b/tests/Images/ReferenceOutput/MeasureTextWordBreakMatchesMDN_238-_layoutMode_HorizontalBottomTop-wordBreaking_Standard_.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:aa871567b5679bc9d5ce3bd15cc75db29f8c426c432f9ee9aade793503db8780 -size 15534 +oid sha256:e50adb0b48de37ea59eaae0f4074ecf04d6259289f7a3c96b6f3a12d2cc0637e +size 11070 diff --git a/tests/Images/ReferenceOutput/MeasureTextWordBreakMatchesMDN_238-_layoutMode_HorizontalTopBottom-wordBreaking_BreakAll_.png b/tests/Images/ReferenceOutput/MeasureTextWordBreakMatchesMDN_238-_layoutMode_HorizontalTopBottom-wordBreaking_BreakAll_.png index decad75e9..1b22905c8 100644 --- a/tests/Images/ReferenceOutput/MeasureTextWordBreakMatchesMDN_238-_layoutMode_HorizontalTopBottom-wordBreaking_BreakAll_.png +++ b/tests/Images/ReferenceOutput/MeasureTextWordBreakMatchesMDN_238-_layoutMode_HorizontalTopBottom-wordBreaking_BreakAll_.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:db1763ea5acad8f687dd0c05f3098537ff70aef6e913c78ac6434c52479a142c -size 13625 +oid sha256:4af56ef400ef112add3dea9c980f7b4efa726525cce6880ade18f428bc37cc1b +size 10940 diff --git a/tests/Images/ReferenceOutput/MeasureTextWordBreakMatchesMDN_238-_layoutMode_HorizontalTopBottom-wordBreaking_BreakWord_.png b/tests/Images/ReferenceOutput/MeasureTextWordBreakMatchesMDN_238-_layoutMode_HorizontalTopBottom-wordBreaking_BreakWord_.png index 944aaaea5..0b8eb26b9 100644 --- a/tests/Images/ReferenceOutput/MeasureTextWordBreakMatchesMDN_238-_layoutMode_HorizontalTopBottom-wordBreaking_BreakWord_.png +++ b/tests/Images/ReferenceOutput/MeasureTextWordBreakMatchesMDN_238-_layoutMode_HorizontalTopBottom-wordBreaking_BreakWord_.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bbd40a382f8bff784c3034b35010657eb24424455b463854aab15084ae299c67 -size 13934 +oid sha256:eeb04b0241a77417596150231ed7b3e29acdbdf4f142ff5d2b7ce2c6bd87c819 +size 10826 diff --git a/tests/Images/ReferenceOutput/MeasureTextWordBreakMatchesMDN_238-_layoutMode_HorizontalTopBottom-wordBreaking_KeepAll_.png b/tests/Images/ReferenceOutput/MeasureTextWordBreakMatchesMDN_238-_layoutMode_HorizontalTopBottom-wordBreaking_KeepAll_.png index e7b097e53..a0c926580 100644 --- a/tests/Images/ReferenceOutput/MeasureTextWordBreakMatchesMDN_238-_layoutMode_HorizontalTopBottom-wordBreaking_KeepAll_.png +++ b/tests/Images/ReferenceOutput/MeasureTextWordBreakMatchesMDN_238-_layoutMode_HorizontalTopBottom-wordBreaking_KeepAll_.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:90d2e30a27f5099653416a9d7d9c8ef9c73d73012fe332060bae34e6c664beb9 -size 14669 +oid sha256:837ea68e12addcd77a56edd3057647734bc51c1dd8b22610503b029e447b57a9 +size 10894 diff --git a/tests/Images/ReferenceOutput/MeasureTextWordBreakMatchesMDN_238-_layoutMode_HorizontalTopBottom-wordBreaking_Standard_.png b/tests/Images/ReferenceOutput/MeasureTextWordBreakMatchesMDN_238-_layoutMode_HorizontalTopBottom-wordBreaking_Standard_.png index 02b9507f0..3815bfe42 100644 --- a/tests/Images/ReferenceOutput/MeasureTextWordBreakMatchesMDN_238-_layoutMode_HorizontalTopBottom-wordBreaking_Standard_.png +++ b/tests/Images/ReferenceOutput/MeasureTextWordBreakMatchesMDN_238-_layoutMode_HorizontalTopBottom-wordBreaking_Standard_.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1f3ce23553e4c523b500bff99d489ec881f86c2c1d804d71d6a3d622efd123eb -size 15836 +oid sha256:f67723e438b0c91861d77b61a5788a63df3a891c5edd7f70637a5266d59fe54a +size 11058 diff --git a/tests/Images/ReferenceOutput/MeasureTextWordBreak_500-_layoutMode_HorizontalBottomTop-wordBreaking_BreakAll_.png b/tests/Images/ReferenceOutput/MeasureTextWordBreak_500-_layoutMode_HorizontalBottomTop-wordBreaking_BreakAll_.png index afc54c24f..f1829b2a7 100644 --- a/tests/Images/ReferenceOutput/MeasureTextWordBreak_500-_layoutMode_HorizontalBottomTop-wordBreaking_BreakAll_.png +++ b/tests/Images/ReferenceOutput/MeasureTextWordBreak_500-_layoutMode_HorizontalBottomTop-wordBreaking_BreakAll_.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:df02acfd86e6cdc342b40d0210d7f8ae3c422a0bf22440098f1e26f7844b5ecc -size 17951 +oid sha256:a4b1724814647514642d297f2ebea706b0b5d91cc170af0c67854f94fa3f1ac3 +size 14661 diff --git a/tests/Images/ReferenceOutput/MeasureTextWordBreak_500-_layoutMode_HorizontalBottomTop-wordBreaking_BreakWord_.png b/tests/Images/ReferenceOutput/MeasureTextWordBreak_500-_layoutMode_HorizontalBottomTop-wordBreaking_BreakWord_.png index 5ce25bdc7..bf9b1fb85 100644 --- a/tests/Images/ReferenceOutput/MeasureTextWordBreak_500-_layoutMode_HorizontalBottomTop-wordBreaking_BreakWord_.png +++ b/tests/Images/ReferenceOutput/MeasureTextWordBreak_500-_layoutMode_HorizontalBottomTop-wordBreaking_BreakWord_.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:40e1e4827299a84adc6c43bf6863b75632d04ca866a5b4ebbe714380ddac8779 -size 19718 +oid sha256:6b9d4d8eb6346b3de9265f87cf9c9dbf604d6ef541b541c832b14bed599e081a +size 14973 diff --git a/tests/Images/ReferenceOutput/MeasureTextWordBreak_500-_layoutMode_HorizontalBottomTop-wordBreaking_KeepAll_.png b/tests/Images/ReferenceOutput/MeasureTextWordBreak_500-_layoutMode_HorizontalBottomTop-wordBreaking_KeepAll_.png index 6f9d745bd..d4d6ac2f5 100644 --- a/tests/Images/ReferenceOutput/MeasureTextWordBreak_500-_layoutMode_HorizontalBottomTop-wordBreaking_KeepAll_.png +++ b/tests/Images/ReferenceOutput/MeasureTextWordBreak_500-_layoutMode_HorizontalBottomTop-wordBreaking_KeepAll_.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7c625b93dac39f6590a4cce658ff789a96b51e889e0ae38498a66776504a9040 -size 12780 +oid sha256:af7e0db06ccd58ba897e2123f4b485c87168790efe0dd1fc1a524a74a4d17fbd +size 8635 diff --git a/tests/Images/ReferenceOutput/MeasureTextWordBreak_500-_layoutMode_HorizontalBottomTop-wordBreaking_Standard_.png b/tests/Images/ReferenceOutput/MeasureTextWordBreak_500-_layoutMode_HorizontalBottomTop-wordBreaking_Standard_.png index a977f9273..887468bc1 100644 --- a/tests/Images/ReferenceOutput/MeasureTextWordBreak_500-_layoutMode_HorizontalBottomTop-wordBreaking_Standard_.png +++ b/tests/Images/ReferenceOutput/MeasureTextWordBreak_500-_layoutMode_HorizontalBottomTop-wordBreaking_Standard_.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cf72e8c03fe1addbbc47bb24e0c535ddb942146bd0491da5ce57ce40b4b4ee03 -size 21232 +oid sha256:3936a7f7a220ca081a5a7cd9b767a431ba0d02dda4bb2a34db97abba0e5f5b82 +size 14845 diff --git a/tests/Images/ReferenceOutput/MeasureTextWordBreak_500-_layoutMode_HorizontalTopBottom-wordBreaking_BreakAll_.png b/tests/Images/ReferenceOutput/MeasureTextWordBreak_500-_layoutMode_HorizontalTopBottom-wordBreaking_BreakAll_.png index eebe262bb..da189cf25 100644 --- a/tests/Images/ReferenceOutput/MeasureTextWordBreak_500-_layoutMode_HorizontalTopBottom-wordBreaking_BreakAll_.png +++ b/tests/Images/ReferenceOutput/MeasureTextWordBreak_500-_layoutMode_HorizontalTopBottom-wordBreaking_BreakAll_.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d7c76a2925cdf4d5b58d501aedfb51701c256feb141a5cfee2b71ab0383c9428 -size 17853 +oid sha256:fbbcebfb4ba9b295839c0a698fee8552486015f7bc2b80cb9dc2cf30a9d0a548 +size 14726 diff --git a/tests/Images/ReferenceOutput/MeasureTextWordBreak_500-_layoutMode_HorizontalTopBottom-wordBreaking_BreakWord_.png b/tests/Images/ReferenceOutput/MeasureTextWordBreak_500-_layoutMode_HorizontalTopBottom-wordBreaking_BreakWord_.png index 472dc0dfb..2b5d02f1d 100644 --- a/tests/Images/ReferenceOutput/MeasureTextWordBreak_500-_layoutMode_HorizontalTopBottom-wordBreaking_BreakWord_.png +++ b/tests/Images/ReferenceOutput/MeasureTextWordBreak_500-_layoutMode_HorizontalTopBottom-wordBreaking_BreakWord_.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d466ce24ae741e9cf1b655c2c58ec072e03b71a443860ce7c3b379bc9b770933 -size 19714 +oid sha256:b5fc1bc480d19845aa12bab760ccef188e287b8e862c00a86976df45ab29e09b +size 15033 diff --git a/tests/Images/ReferenceOutput/MeasureTextWordBreak_500-_layoutMode_HorizontalTopBottom-wordBreaking_KeepAll_.png b/tests/Images/ReferenceOutput/MeasureTextWordBreak_500-_layoutMode_HorizontalTopBottom-wordBreaking_KeepAll_.png index 7f079ebc8..62d4ffe67 100644 --- a/tests/Images/ReferenceOutput/MeasureTextWordBreak_500-_layoutMode_HorizontalTopBottom-wordBreaking_KeepAll_.png +++ b/tests/Images/ReferenceOutput/MeasureTextWordBreak_500-_layoutMode_HorizontalTopBottom-wordBreaking_KeepAll_.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:34ca63298f5f367b0caa4a6319b310f674b4e453da5cebaa43dfd57dfcca89a3 -size 20600 +oid sha256:4ef7de02d3584823406905407e22287717fbe94de6ce148f76523508cbc0d671 +size 14749 diff --git a/tests/Images/ReferenceOutput/MeasureTextWordBreak_500-_layoutMode_HorizontalTopBottom-wordBreaking_Standard_.png b/tests/Images/ReferenceOutput/MeasureTextWordBreak_500-_layoutMode_HorizontalTopBottom-wordBreaking_Standard_.png index 7b3e0b93b..1c70623b7 100644 --- a/tests/Images/ReferenceOutput/MeasureTextWordBreak_500-_layoutMode_HorizontalTopBottom-wordBreaking_Standard_.png +++ b/tests/Images/ReferenceOutput/MeasureTextWordBreak_500-_layoutMode_HorizontalTopBottom-wordBreaking_Standard_.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:63a36c05d870c9b9a1a5535865d436a85bd016fe1f3a90c73be80c1bb5b5d347 -size 21414 +oid sha256:9fc7f1bcfc1ed4d243156520b527f7ce13cf511bc6a0b3d8a23ce13f75adae3d +size 14865 diff --git a/tests/Images/ReferenceOutput/MeasureTextWordWrappingHorizontalBottomTop_350-_height_10-width_87.125_.png b/tests/Images/ReferenceOutput/MeasureTextWordWrappingHorizontalBottomTop_350-_height_10-width_87.125_.png index 6d63b32da..1090eff38 100644 --- a/tests/Images/ReferenceOutput/MeasureTextWordWrappingHorizontalBottomTop_350-_height_10-width_87.125_.png +++ b/tests/Images/ReferenceOutput/MeasureTextWordWrappingHorizontalBottomTop_350-_height_10-width_87.125_.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4aca35883069b0d04ee419319ff44ba10fe60129369da2d78dd395d5d1e46c08 -size 903 +oid sha256:9f909d64aa7f6d2bbfb8ff8aa7b9b7ab0ae2c5ea014f6715c8456dc464c51ca2 +size 878 diff --git a/tests/Images/ReferenceOutput/MeasureTextWordWrappingHorizontalBottomTop_350-_height_11.438-width_279.13_.png b/tests/Images/ReferenceOutput/MeasureTextWordWrappingHorizontalBottomTop_350-_height_11.438-width_279.13_.png index 514f92ce4..662fc493d 100644 --- a/tests/Images/ReferenceOutput/MeasureTextWordWrappingHorizontalBottomTop_350-_height_11.438-width_279.13_.png +++ b/tests/Images/ReferenceOutput/MeasureTextWordWrappingHorizontalBottomTop_350-_height_11.438-width_279.13_.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:64acc2c6102b2d5bce5c1d01bb3ac4e09b48541d9bb484d5f830901053337a04 -size 949 +oid sha256:04f2dff257b9b4ae960136eb13f6e8a2893a3ad1925bde9bbefde11fe6223cc0 +size 964 diff --git a/tests/Images/ReferenceOutput/MeasureTextWordWrappingHorizontalBottomTop_350-_height_62.625-width_318.86_.png b/tests/Images/ReferenceOutput/MeasureTextWordWrappingHorizontalBottomTop_350-_height_62.625-width_318.86_.png index 6f2d5725c..04113eba5 100644 --- a/tests/Images/ReferenceOutput/MeasureTextWordWrappingHorizontalBottomTop_350-_height_62.625-width_318.86_.png +++ b/tests/Images/ReferenceOutput/MeasureTextWordWrappingHorizontalBottomTop_350-_height_62.625-width_318.86_.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:08a12fda71304bb3bc790b5d37e9ff03691bca5ea8994fdcb7354696e4637357 -size 7021 +oid sha256:2fa8cad5b692d6ef4e6a1910faa47d227c8f59b664140fd02af6a3fe848dc3d9 +size 6925 diff --git a/tests/Images/ReferenceOutput/MeasureTextWordWrappingHorizontalTopBottom_350-_height_10-width_87.125_.png b/tests/Images/ReferenceOutput/MeasureTextWordWrappingHorizontalTopBottom_350-_height_10-width_87.125_.png index 6d63b32da..1090eff38 100644 --- a/tests/Images/ReferenceOutput/MeasureTextWordWrappingHorizontalTopBottom_350-_height_10-width_87.125_.png +++ b/tests/Images/ReferenceOutput/MeasureTextWordWrappingHorizontalTopBottom_350-_height_10-width_87.125_.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4aca35883069b0d04ee419319ff44ba10fe60129369da2d78dd395d5d1e46c08 -size 903 +oid sha256:9f909d64aa7f6d2bbfb8ff8aa7b9b7ab0ae2c5ea014f6715c8456dc464c51ca2 +size 878 diff --git a/tests/Images/ReferenceOutput/MeasureTextWordWrappingHorizontalTopBottom_350-_height_11.438-width_279.13_.png b/tests/Images/ReferenceOutput/MeasureTextWordWrappingHorizontalTopBottom_350-_height_11.438-width_279.13_.png index 514f92ce4..662fc493d 100644 --- a/tests/Images/ReferenceOutput/MeasureTextWordWrappingHorizontalTopBottom_350-_height_11.438-width_279.13_.png +++ b/tests/Images/ReferenceOutput/MeasureTextWordWrappingHorizontalTopBottom_350-_height_11.438-width_279.13_.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:64acc2c6102b2d5bce5c1d01bb3ac4e09b48541d9bb484d5f830901053337a04 -size 949 +oid sha256:04f2dff257b9b4ae960136eb13f6e8a2893a3ad1925bde9bbefde11fe6223cc0 +size 964 diff --git a/tests/Images/ReferenceOutput/MeasureTextWordWrappingHorizontalTopBottom_350-_height_62.625-width_318.86_.png b/tests/Images/ReferenceOutput/MeasureTextWordWrappingHorizontalTopBottom_350-_height_62.625-width_318.86_.png index cbd193aa4..bf7bc67b8 100644 --- a/tests/Images/ReferenceOutput/MeasureTextWordWrappingHorizontalTopBottom_350-_height_62.625-width_318.86_.png +++ b/tests/Images/ReferenceOutput/MeasureTextWordWrappingHorizontalTopBottom_350-_height_62.625-width_318.86_.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c9ab09c566f3ba1655d9ddd6fab49fc32c42dd2846c28a10cfb97cf030224687 -size 9210 +oid sha256:acf6e569fb1283da68fcc3a216920335212f20e81392ecef0e0e6da08c72b13a +size 6824 diff --git a/tests/Images/ReferenceOutput/MeasureTextWordWrappingVerticalLeftRight_350-_height_171.25-width_10_.png b/tests/Images/ReferenceOutput/MeasureTextWordWrappingVerticalLeftRight_350-_height_171.25-width_10_.png index 41732a0ea..ee72baa2b 100644 --- a/tests/Images/ReferenceOutput/MeasureTextWordWrappingVerticalLeftRight_350-_height_171.25-width_10_.png +++ b/tests/Images/ReferenceOutput/MeasureTextWordWrappingVerticalLeftRight_350-_height_171.25-width_10_.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2c734cb47170489a2ae2b39ffcafd236c190daf7b93c4ab212544a9ef0043908 -size 885 +oid sha256:36e3e9bd0177ddf73b996cb118bf37359e13f5d666420439fc1f42898b56987c +size 854 diff --git a/tests/Images/ReferenceOutput/MeasureTextWordWrappingVerticalLeftRight_350-_height_267.25-width_23.875_.png b/tests/Images/ReferenceOutput/MeasureTextWordWrappingVerticalLeftRight_350-_height_267.25-width_23.875_.png index 9db97c59f..9b5aaef98 100644 --- a/tests/Images/ReferenceOutput/MeasureTextWordWrappingVerticalLeftRight_350-_height_267.25-width_23.875_.png +++ b/tests/Images/ReferenceOutput/MeasureTextWordWrappingVerticalLeftRight_350-_height_267.25-width_23.875_.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8f70c2f647d555bef06d632b5d258fb3a486e39b4c6fa0b0d8309479c6568600 -size 1121 +oid sha256:b20e74691e596e06b9fd223664fd37f6589fadf08978b3677f3bca11b37b34d4 +size 1090 diff --git a/tests/Images/ReferenceOutput/MeasureTextWordWrappingVerticalLeftRight_350-_height_318.563-width_62.813_.png b/tests/Images/ReferenceOutput/MeasureTextWordWrappingVerticalLeftRight_350-_height_318.563-width_62.813_.png index cbca8bdb9..4b8b572ea 100644 --- a/tests/Images/ReferenceOutput/MeasureTextWordWrappingVerticalLeftRight_350-_height_318.563-width_62.813_.png +++ b/tests/Images/ReferenceOutput/MeasureTextWordWrappingVerticalLeftRight_350-_height_318.563-width_62.813_.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6899c7213d85b6f4c9387cce6b86cfff27924f99937280572ff4d6de6a6fa395 -size 7001 +oid sha256:ff75fe5c976d2858e9cd29b16ee9d60fc5152c128bf703465b916226c446bc48 +size 6909 diff --git a/tests/Images/ReferenceOutput/MeasureTextWordWrappingVerticalMixedLeftRight_350-_height_279.125-width_11.438_.png b/tests/Images/ReferenceOutput/MeasureTextWordWrappingVerticalMixedLeftRight_350-_height_279.125-width_11.438_.png index 762e925ff..701d6c929 100644 --- a/tests/Images/ReferenceOutput/MeasureTextWordWrappingVerticalMixedLeftRight_350-_height_279.125-width_11.438_.png +++ b/tests/Images/ReferenceOutput/MeasureTextWordWrappingVerticalMixedLeftRight_350-_height_279.125-width_11.438_.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e74fc585d162786ef08b123b074a22c51dc5248d821c7503b20a596ded093cab -size 910 +oid sha256:77ce2c51367958b9f889459b508585ca99e798a2efdea06ec4d158cf506888a4 +size 922 diff --git a/tests/Images/ReferenceOutput/MeasureTextWordWrappingVerticalMixedLeftRight_350-_height_318.563-width_62.813_.png b/tests/Images/ReferenceOutput/MeasureTextWordWrappingVerticalMixedLeftRight_350-_height_318.563-width_62.813_.png index cbca8bdb9..4b8b572ea 100644 --- a/tests/Images/ReferenceOutput/MeasureTextWordWrappingVerticalMixedLeftRight_350-_height_318.563-width_62.813_.png +++ b/tests/Images/ReferenceOutput/MeasureTextWordWrappingVerticalMixedLeftRight_350-_height_318.563-width_62.813_.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6899c7213d85b6f4c9387cce6b86cfff27924f99937280572ff4d6de6a6fa395 -size 7001 +oid sha256:ff75fe5c976d2858e9cd29b16ee9d60fc5152c128bf703465b916226c446bc48 +size 6909 diff --git a/tests/Images/ReferenceOutput/MeasureTextWordWrappingVerticalMixedLeftRight_350-_height_87.125-width_10_.png b/tests/Images/ReferenceOutput/MeasureTextWordWrappingVerticalMixedLeftRight_350-_height_87.125-width_10_.png index f9d3c5a5c..abdadb05a 100644 --- a/tests/Images/ReferenceOutput/MeasureTextWordWrappingVerticalMixedLeftRight_350-_height_87.125-width_10_.png +++ b/tests/Images/ReferenceOutput/MeasureTextWordWrappingVerticalMixedLeftRight_350-_height_87.125-width_10_.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b39e904a69b96153bbdb8a64790b967ba4b7a53531556f54454a65619d058d56 -size 891 +oid sha256:b0bb7b04b88e9d8d6ad842702cafaff290febda2feca438d85bc0136c28d3b37 +size 863 diff --git a/tests/Images/ReferenceOutput/MeasureTextWordWrappingVerticalRightLeft_350-_height_171.25-width_10_.png b/tests/Images/ReferenceOutput/MeasureTextWordWrappingVerticalRightLeft_350-_height_171.25-width_10_.png index 41732a0ea..ee72baa2b 100644 --- a/tests/Images/ReferenceOutput/MeasureTextWordWrappingVerticalRightLeft_350-_height_171.25-width_10_.png +++ b/tests/Images/ReferenceOutput/MeasureTextWordWrappingVerticalRightLeft_350-_height_171.25-width_10_.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2c734cb47170489a2ae2b39ffcafd236c190daf7b93c4ab212544a9ef0043908 -size 885 +oid sha256:36e3e9bd0177ddf73b996cb118bf37359e13f5d666420439fc1f42898b56987c +size 854 diff --git a/tests/Images/ReferenceOutput/MeasureTextWordWrappingVerticalRightLeft_350-_height_267.25-width_23.875_.png b/tests/Images/ReferenceOutput/MeasureTextWordWrappingVerticalRightLeft_350-_height_267.25-width_23.875_.png index e7f0ece17..f910768db 100644 --- a/tests/Images/ReferenceOutput/MeasureTextWordWrappingVerticalRightLeft_350-_height_267.25-width_23.875_.png +++ b/tests/Images/ReferenceOutput/MeasureTextWordWrappingVerticalRightLeft_350-_height_267.25-width_23.875_.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:684b6f8d67daf40fa838c903dd3051e62b251b6c306f8944417b009a66c0c6b1 -size 1122 +oid sha256:49800f0bab1fa5c6252c36fb5e473f459ce5e3deb4f5b828bc758d205819357d +size 1091 diff --git a/tests/Images/ReferenceOutput/MeasureTextWordWrappingVerticalRightLeft_350-_height_318.563-width_62.813_.png b/tests/Images/ReferenceOutput/MeasureTextWordWrappingVerticalRightLeft_350-_height_318.563-width_62.813_.png index 4b9e59c14..06f4fd8c1 100644 --- a/tests/Images/ReferenceOutput/MeasureTextWordWrappingVerticalRightLeft_350-_height_318.563-width_62.813_.png +++ b/tests/Images/ReferenceOutput/MeasureTextWordWrappingVerticalRightLeft_350-_height_318.563-width_62.813_.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e3fec4e5c1b7095e1a61a9282285a21ec0725bd75c251b87fe9c6f9033568eec -size 7053 +oid sha256:580c3a6f49c2f40ebb283a7b67696d342cf08c01f85acdd5de6aff78c220d6a1 +size 6916 diff --git a/tests/Images/ReferenceOutput/RenderingTextIncludesAllGlyphs_1900.png b/tests/Images/ReferenceOutput/RenderingTextIncludesAllGlyphs_1900.png index d2c6cb17f..80f4b03a3 100644 --- a/tests/Images/ReferenceOutput/RenderingTextIncludesAllGlyphs_1900.png +++ b/tests/Images/ReferenceOutput/RenderingTextIncludesAllGlyphs_1900.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4d454b807a87435d8fc7e7f4d9871100723d6c5e93d92eadb267ab878ead48eb -size 38485 +oid sha256:fc0f874d550d3631d31a402fca870a4ccff14d7bf409b511136b2380ff5b4931 +size 35370 diff --git a/tests/Images/ReferenceOutput/ShouldBreakIntoTwoLinesA_1000.png b/tests/Images/ReferenceOutput/ShouldBreakIntoTwoLinesA_1000.png index 15d1b79a1..6869e0ab2 100644 --- a/tests/Images/ReferenceOutput/ShouldBreakIntoTwoLinesA_1000.png +++ b/tests/Images/ReferenceOutput/ShouldBreakIntoTwoLinesA_1000.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:26987f3e7f3a4a7d21c32cb0fc313a8b3a5bae156fb13ecfcbdf125d53f9db32 -size 22122 +oid sha256:03dc862ef3c09d8a1c987ded5fdeaab90a3b499fb296c0b1f6a5a8bcf40aaa60 +size 13509 diff --git a/tests/Images/ReferenceOutput/ShouldBreakIntoTwoLinesB_100.png b/tests/Images/ReferenceOutput/ShouldBreakIntoTwoLinesB_100.png index 09b63dab3..4f1757723 100644 --- a/tests/Images/ReferenceOutput/ShouldBreakIntoTwoLinesB_100.png +++ b/tests/Images/ReferenceOutput/ShouldBreakIntoTwoLinesB_100.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2854877e7f02ebe2bfaa1e2347957377ff0955a3a0115af891ddd1f84f75e8d5 -size 2371 +oid sha256:23884e2fdad64f3bea9caeff2d2a83f7cf9910672d262a5b24435037dd1ebf00 +size 2260 diff --git a/tests/Images/ReferenceOutput/ShouldInsertExtraLineBreaksA_400-5.png b/tests/Images/ReferenceOutput/ShouldInsertExtraLineBreaksA_400-5.png index 5c1b39b45..a37afc3eb 100644 --- a/tests/Images/ReferenceOutput/ShouldInsertExtraLineBreaksA_400-5.png +++ b/tests/Images/ReferenceOutput/ShouldInsertExtraLineBreaksA_400-5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:92055deca14e9f0434a4c740dce925b33dfbb49e62b318bcd36c9246c1d10d9e -size 15931 +oid sha256:0663f259f7c35a9e1aaaa0c91ea523f4846dd224c7184df702c82823705f2126 +size 9957 diff --git a/tests/Images/ReferenceOutput/ShouldInsertExtraLineBreaksB_400-6.png b/tests/Images/ReferenceOutput/ShouldInsertExtraLineBreaksB_400-6.png index 8ef0c5b2c..482fd0727 100644 --- a/tests/Images/ReferenceOutput/ShouldInsertExtraLineBreaksB_400-6.png +++ b/tests/Images/ReferenceOutput/ShouldInsertExtraLineBreaksB_400-6.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b4ec5fb262e0a0af3beec4b4225be2e7921cc51105e10e66b94ac5eb4ad9dc68 -size 16207 +oid sha256:5ca42b922777be8e473ddeaa2678afd572cd37c28b40a91837e9cf43ca3b72ed +size 10118 diff --git a/tests/Images/ReferenceOutput/ShouldMatchBrowserBreak_372.png b/tests/Images/ReferenceOutput/ShouldMatchBrowserBreak_372.png index 62e60fed9..4ba35ba5a 100644 --- a/tests/Images/ReferenceOutput/ShouldMatchBrowserBreak_372.png +++ b/tests/Images/ReferenceOutput/ShouldMatchBrowserBreak_372.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1a63f2c0fcefe3a3359cc8c3f154569bd1035b7a5d77348db23873b1b43c8fe1 -size 5929 +oid sha256:57685c8326b78b4f5df4555d6e3fd00a30a30c483fce71c64908879676dfcb72 +size 4578 diff --git a/tests/Images/ReferenceOutput/ShouldNotInsertExtraLineBreaks_2_400.png b/tests/Images/ReferenceOutput/ShouldNotInsertExtraLineBreaks_2_400.png index eaff4b43d..f632d0d2f 100644 --- a/tests/Images/ReferenceOutput/ShouldNotInsertExtraLineBreaks_2_400.png +++ b/tests/Images/ReferenceOutput/ShouldNotInsertExtraLineBreaks_2_400.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:978a2c1059e637e1687b4eb65861dba533a40fd564cb38ed0c971018b8e51e38 -size 15243 +oid sha256:b12046a70eb8f57f5db6b4501e2d7b7213e76482bc0f18d8b340219c8faf9137 +size 9613 diff --git a/tests/Images/ReferenceOutput/ShouldNotInsertExtraLineBreaks_400.png b/tests/Images/ReferenceOutput/ShouldNotInsertExtraLineBreaks_400.png index eaff4b43d..f632d0d2f 100644 --- a/tests/Images/ReferenceOutput/ShouldNotInsertExtraLineBreaks_400.png +++ b/tests/Images/ReferenceOutput/ShouldNotInsertExtraLineBreaks_400.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:978a2c1059e637e1687b4eb65861dba533a40fd564cb38ed0c971018b8e51e38 -size 15243 +oid sha256:b12046a70eb8f57f5db6b4501e2d7b7213e76482bc0f18d8b340219c8faf9137 +size 9613 diff --git a/tests/Images/ReferenceOutput/TestIssue_468-.png b/tests/Images/ReferenceOutput/TestIssue_468-.png index b699b0680..83473c94d 100644 --- a/tests/Images/ReferenceOutput/TestIssue_468-.png +++ b/tests/Images/ReferenceOutput/TestIssue_468-.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2a9c859567eb782a8f933289cb134acbfe71053ff2c6cda70f5112599d1ab355 -size 19695 +oid sha256:f8b36c3c0e8b392a7a1b89b90bf7dd1f8f7e249ddc2503c1f4e3417e1f60efa8 +size 19384 diff --git a/tests/Images/ReferenceOutput/Test_Hinting_Robustness_-Arial-.png b/tests/Images/ReferenceOutput/Test_Hinting_Robustness_-Arial-.png index ccab2f6d6..2d6c0b48a 100644 --- a/tests/Images/ReferenceOutput/Test_Hinting_Robustness_-Arial-.png +++ b/tests/Images/ReferenceOutput/Test_Hinting_Robustness_-Arial-.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:af456c8d093b4a30b878f6c8b5f9b0d4b03648b6b2e1a17c86c70c89c5fe1f26 -size 299643 +oid sha256:37dd8ab9cc33aaa943f87ef52c1db18f2dc437047be4227a9f60295f8f4ef4a5 +size 303399 diff --git a/tests/Images/ReferenceOutput/Test_Hinting_Robustness_-OpenSansFile-.png b/tests/Images/ReferenceOutput/Test_Hinting_Robustness_-OpenSansFile-.png index f433ccfc6..a33ec7d28 100644 --- a/tests/Images/ReferenceOutput/Test_Hinting_Robustness_-OpenSansFile-.png +++ b/tests/Images/ReferenceOutput/Test_Hinting_Robustness_-OpenSansFile-.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eb34a55a1a282a9f15441584471b80b25a200d75e8882e9616421a15c9141fbd -size 311938 +oid sha256:c203c84329f4879c9c2c5c8383a6d570a9393691c756ae0551986b1d0e0acf5b +size 313349 diff --git a/tests/Images/ReferenceOutput/Test_Hinting_Robustness_-Tahoma-.png b/tests/Images/ReferenceOutput/Test_Hinting_Robustness_-Tahoma-.png index efd35be79..1017b7915 100644 --- a/tests/Images/ReferenceOutput/Test_Hinting_Robustness_-Tahoma-.png +++ b/tests/Images/ReferenceOutput/Test_Hinting_Robustness_-Tahoma-.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:da79aba3e589d2959cf729fb71cb5d8f748f68ad80e07fa886dfaa52debe2a76 -size 292765 +oid sha256:97f182fe7a90311c93fa506c0e1513bbdb7baf69555e1ce071471ff4d9ee046a +size 294837 diff --git a/tests/Images/ReferenceOutput/Test_Issue_353_400.png b/tests/Images/ReferenceOutput/Test_Issue_353_400.png index 0acc60665..c37973cb0 100644 --- a/tests/Images/ReferenceOutput/Test_Issue_353_400.png +++ b/tests/Images/ReferenceOutput/Test_Issue_353_400.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3e641628fd5a1d1c613c812918bc4fd92882142e29d84dc35c300f55d581a931 -size 17664 +oid sha256:ef4361d0faa2f0ddb2fe2ee7ea36e2e2b53895b3ad45487475233241bbbb9039 +size 16971 diff --git a/tests/Images/ReferenceOutput/Test_Issue_469-.png b/tests/Images/ReferenceOutput/Test_Issue_469-.png index 3ff1d7729..be99125dc 100644 --- a/tests/Images/ReferenceOutput/Test_Issue_469-.png +++ b/tests/Images/ReferenceOutput/Test_Issue_469-.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5ceb941fbbd9b68a39996ee5e1c1c31e675e34f1e6fb4a5bc602530739b3efee -size 48008 +oid sha256:2e21ddf5656c1ef7408db49b88ce5c469afca4d69b6967d1896c4bb20ecd0469 +size 48042 diff --git a/tests/Images/ReferenceOutput/Test_Issue_470-.png b/tests/Images/ReferenceOutput/Test_Issue_470-.png index 47d7effb4..57c06f07c 100644 --- a/tests/Images/ReferenceOutput/Test_Issue_470-.png +++ b/tests/Images/ReferenceOutput/Test_Issue_470-.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6fb4f94db5afa78077105b002eba6da2401e111691c4b698978a01f7145da81c -size 36606 +oid sha256:2956c21644c7b3b96dca7f70d1d1564390e6118567d7467f98e00b8bcab2e87a +size 34689 diff --git a/tests/Images/ReferenceOutput/Test_Issue_474-.png b/tests/Images/ReferenceOutput/Test_Issue_474-.png index cb317b574..53e201831 100644 --- a/tests/Images/ReferenceOutput/Test_Issue_474-.png +++ b/tests/Images/ReferenceOutput/Test_Issue_474-.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a0b67e85dc7996a7cd02f741d2f6f4da2f8a78677a4850d911857aa4d1559798 -size 4878 +oid sha256:b8146f3ea6270430b5e3a45f754183f11c71de6cb91b1b747b673b6db2b7fc49 +size 5049 diff --git a/tests/Images/ReferenceOutput/Test_Issue_483-.png b/tests/Images/ReferenceOutput/Test_Issue_483-.png index e9b2e0ac4..8940b909a 100644 --- a/tests/Images/ReferenceOutput/Test_Issue_483-.png +++ b/tests/Images/ReferenceOutput/Test_Issue_483-.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c233ae41af155a9175b50d34ab20b9cc92c8628dfba1e7a08c2075996c2d1b98 -size 8179 +oid sha256:a81c48545a6d59de3eebe1ea8be8d024dcc172d424bc2656fbcfc64b796cfbef +size 7915 diff --git a/tests/Images/ReferenceOutput/Test_Issue_488_Bengali-.png b/tests/Images/ReferenceOutput/Test_Issue_488_Bengali-.png index ba539e1c3..6fbe28013 100644 --- a/tests/Images/ReferenceOutput/Test_Issue_488_Bengali-.png +++ b/tests/Images/ReferenceOutput/Test_Issue_488_Bengali-.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:182aa6df102e117ca73450ceba03b91a23fea5bebcc42c3f0aa7fe6eff50034c -size 6121 +oid sha256:72386b93f20eaf53931a9798886ee013567487002e541460de8e310b5921f6ca +size 6029 diff --git a/tests/Images/ReferenceOutput/Test_Issue_488_Hebrew-.png b/tests/Images/ReferenceOutput/Test_Issue_488_Hebrew-.png index d28a7703d..79421d8c8 100644 --- a/tests/Images/ReferenceOutput/Test_Issue_488_Hebrew-.png +++ b/tests/Images/ReferenceOutput/Test_Issue_488_Hebrew-.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5fe7285bc200ca508c22953719fa5b27473fbed926f7687361230a8c4a88ec23 -size 2904 +oid sha256:9cab28cbea986420506d167041212088484c923f90b6bb9a570cfa44ea108367 +size 3255 diff --git a/tests/Images/ReferenceOutput/Test_Issue_488_Lao-.png b/tests/Images/ReferenceOutput/Test_Issue_488_Lao-.png index e922f1fa1..2fcbf00e2 100644 --- a/tests/Images/ReferenceOutput/Test_Issue_488_Lao-.png +++ b/tests/Images/ReferenceOutput/Test_Issue_488_Lao-.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f98fc54194fd0323f1fdd2d092d0e2da6b9dd9a52dec62d323ee115dcf1fa5e9 -size 7056 +oid sha256:82f6c353078256e7f767dc20ab236ab7215878d72f220175309259e7febddced +size 7010 diff --git a/tests/Images/ReferenceOutput/Test_Issue_488_Myanmar-.png b/tests/Images/ReferenceOutput/Test_Issue_488_Myanmar-.png index 1aa13a896..d017414aa 100644 --- a/tests/Images/ReferenceOutput/Test_Issue_488_Myanmar-.png +++ b/tests/Images/ReferenceOutput/Test_Issue_488_Myanmar-.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ce606aa9f4faf75d7260c3388d5662dd102933856f831a48db3e990f58de72c9 -size 6937 +oid sha256:97dbba280f22949712d6c67e897c8922c6e7654dda4e971e5ea03bb9f3a9c5ea +size 7032 diff --git a/tests/Images/ReferenceOutput/Test_Issue_488_Sinhala-.png b/tests/Images/ReferenceOutput/Test_Issue_488_Sinhala-.png index 0dce0d93d..5a70d0c03 100644 --- a/tests/Images/ReferenceOutput/Test_Issue_488_Sinhala-.png +++ b/tests/Images/ReferenceOutput/Test_Issue_488_Sinhala-.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a65dccc53a1191ab532cbe73b465c70aef0673d52d04b7640ad21e127c516a4d -size 12284 +oid sha256:46719380a221e9c472828a154d44e04ee83ca990a8188b1e53997b13661f079e +size 11973 diff --git a/tests/Images/ReferenceOutput/Test_Issue_488_Tibetan-.png b/tests/Images/ReferenceOutput/Test_Issue_488_Tibetan-.png index 2ea058eb1..43ae24d59 100644 --- a/tests/Images/ReferenceOutput/Test_Issue_488_Tibetan-.png +++ b/tests/Images/ReferenceOutput/Test_Issue_488_Tibetan-.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2fe9363ec5ffe9786602ce55af503cf81635ef801499e89dcbad138080d7c1cf -size 3844 +oid sha256:751e0b10854b56fa6805e2fd6fb743a930e46212d9d838dc7482766f5e33db12 +size 3812 diff --git a/tests/Images/ReferenceOutput/Test_Issue_493_MgOpenCanonic-.png b/tests/Images/ReferenceOutput/Test_Issue_493_MgOpenCanonic-.png index 8fe971c08..944264042 100644 --- a/tests/Images/ReferenceOutput/Test_Issue_493_MgOpenCanonic-.png +++ b/tests/Images/ReferenceOutput/Test_Issue_493_MgOpenCanonic-.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d25162d43717d480a84782a3d45026f07ed9b9254daee336d3b2dd3afdd9956c -size 4235 +oid sha256:46311d43daf7678ba16c04beba0bd9fd4432a1018450985c871329d58425cc99 +size 4244 diff --git a/tests/Images/ReferenceOutput/Test_Issue_493_Ogham-.png b/tests/Images/ReferenceOutput/Test_Issue_493_Ogham-.png index 3ce7e1039..6cfb2f70f 100644 --- a/tests/Images/ReferenceOutput/Test_Issue_493_Ogham-.png +++ b/tests/Images/ReferenceOutput/Test_Issue_493_Ogham-.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3fbe7eac8ecd7c133b33a23243d186d63aae40976498ad6c22990d0948a4ab37 -size 1770 +oid sha256:cee0d26e2a69c781a207fe8cb31c749e67cb319f4394b1ebc881f2183369d7e2 +size 1750 diff --git a/tests/Images/ReferenceOutput/Test_Issue_493_Runic-.png b/tests/Images/ReferenceOutput/Test_Issue_493_Runic-.png index 7d927ab94..55a4007d4 100644 --- a/tests/Images/ReferenceOutput/Test_Issue_493_Runic-.png +++ b/tests/Images/ReferenceOutput/Test_Issue_493_Runic-.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:aefe66ece241e0026a003b4d622d2b9dd83a9e0cc4820094c2d74337988d2e3a -size 2465 +oid sha256:314c0c93387054f86fa1d3800973f2f61e396fdbbe6fe47a0dfddcb5e101a2cc +size 2236 diff --git a/tests/Images/ReferenceOutput/TextAlignmentSample_RendersReferenceImage-.png b/tests/Images/ReferenceOutput/TextAlignmentSample_RendersReferenceImage-.png index 4ac351e81..8bd22658f 100644 --- a/tests/Images/ReferenceOutput/TextAlignmentSample_RendersReferenceImage-.png +++ b/tests/Images/ReferenceOutput/TextAlignmentSample_RendersReferenceImage-.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4bde0ea0385ccea8ded2e375163240d083f3b1a2c3c9b6ebe7b5fc177d6efec2 -size 83241 +oid sha256:d4beba082b15dc190d64f1d1a906543bda8047b0b6e3cab664185073ce389a7a +size 79764 diff --git a/tests/Images/ReferenceOutput/TextAlignmentWrapped_RendersReferenceImage-.png b/tests/Images/ReferenceOutput/TextAlignmentWrapped_RendersReferenceImage-.png index adb1d332b..5caa604c3 100644 --- a/tests/Images/ReferenceOutput/TextAlignmentWrapped_RendersReferenceImage-.png +++ b/tests/Images/ReferenceOutput/TextAlignmentWrapped_RendersReferenceImage-.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e726768dd70b15f75a89a2e6b0b19a4d1130b81d9a24df837f7320aca8900637 -size 151879 +oid sha256:736cc68a4eccc5d4e825368c748aa884e4fdcd6c80b1813c39b3f256c33e3424 +size 151859 diff --git a/tests/Images/ReferenceOutput/TextBidiMode_DrawsMixedBidiLayout_430--ltr-normal-.png b/tests/Images/ReferenceOutput/TextBidiMode_DrawsMixedBidiLayout_430--ltr-normal-.png index f3bcc1dde..d99b84265 100644 --- a/tests/Images/ReferenceOutput/TextBidiMode_DrawsMixedBidiLayout_430--ltr-normal-.png +++ b/tests/Images/ReferenceOutput/TextBidiMode_DrawsMixedBidiLayout_430--ltr-normal-.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b232ec69127ab2ec6d3f2740cf1fe266d21ee78ce6a114e0254d00ea9d899087 -size 3573 +oid sha256:f0f802b93a163ab94bf7c670c9a4001ec4cf926b3cb149f8b64ad0319fee20d2 +size 3455 diff --git a/tests/Images/ReferenceOutput/TextBidiMode_DrawsMixedBidiLayout_430--ltr-override-.png b/tests/Images/ReferenceOutput/TextBidiMode_DrawsMixedBidiLayout_430--ltr-override-.png index 83e71f5ae..6ab7b1390 100644 --- a/tests/Images/ReferenceOutput/TextBidiMode_DrawsMixedBidiLayout_430--ltr-override-.png +++ b/tests/Images/ReferenceOutput/TextBidiMode_DrawsMixedBidiLayout_430--ltr-override-.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:46bdb2f935fbfeab907dd0be764aedd275d58e8251ba0ded98ff0c939951914b -size 3573 +oid sha256:a26b26267d52040c53bc97ed61c3a68ca28a3a8bb06d41a320cf5ef8cddd727a +size 3458 diff --git a/tests/Images/ReferenceOutput/TextBidiMode_DrawsMixedBidiLayout_430--rtl-normal-.png b/tests/Images/ReferenceOutput/TextBidiMode_DrawsMixedBidiLayout_430--rtl-normal-.png index be4508d0b..2fc06646b 100644 --- a/tests/Images/ReferenceOutput/TextBidiMode_DrawsMixedBidiLayout_430--rtl-normal-.png +++ b/tests/Images/ReferenceOutput/TextBidiMode_DrawsMixedBidiLayout_430--rtl-normal-.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:603d99b4c937cc0e6210734a2d8251c06211231e1f484e48a5c866b23660edaf -size 3622 +oid sha256:262f591c8ef4b1c9aaf9ddcd4f7b54bb49b6afe0433fb578083df920f32e3c0a +size 3506 diff --git a/tests/Images/ReferenceOutput/TextBidiMode_DrawsMixedBidiLayout_430--rtl-override-.png b/tests/Images/ReferenceOutput/TextBidiMode_DrawsMixedBidiLayout_430--rtl-override-.png index 9f3faa4b0..2ee9de565 100644 --- a/tests/Images/ReferenceOutput/TextBidiMode_DrawsMixedBidiLayout_430--rtl-override-.png +++ b/tests/Images/ReferenceOutput/TextBidiMode_DrawsMixedBidiLayout_430--rtl-override-.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:015b2369dc710216e003903e590191529229823810e214cce02034252e0d9e95 -size 3582 +oid sha256:8e8429786fb3a65ff92ebabf8dad09ec7b36a40c16810fc933078881a61f75de +size 3461 diff --git a/tests/Images/ReferenceOutput/TextEllipsis_DrawsMaxLinesMarker_210--custom-.png b/tests/Images/ReferenceOutput/TextEllipsis_DrawsMaxLinesMarker_210--custom-.png index 4f01aa25e..6f0b81c8f 100644 --- a/tests/Images/ReferenceOutput/TextEllipsis_DrawsMaxLinesMarker_210--custom-.png +++ b/tests/Images/ReferenceOutput/TextEllipsis_DrawsMaxLinesMarker_210--custom-.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:96838c315761d94119296f66fda521b687045d61772c9cc04d4eb2598e60dd02 -size 2286 +oid sha256:06ff63b05a03e7522aec0d9f0d156aa27b3288d61a562de17a12fcc07f57f001 +size 2195 diff --git a/tests/Images/ReferenceOutput/TextEllipsis_DrawsMaxLinesMarker_210--none-.png b/tests/Images/ReferenceOutput/TextEllipsis_DrawsMaxLinesMarker_210--none-.png index e91fb3fd3..3426b7924 100644 --- a/tests/Images/ReferenceOutput/TextEllipsis_DrawsMaxLinesMarker_210--none-.png +++ b/tests/Images/ReferenceOutput/TextEllipsis_DrawsMaxLinesMarker_210--none-.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6761228a6cf4e08ab5b2785bba120b8eda1f5a26f6f667c17b2d0ee245ef7990 -size 2125 +oid sha256:718c9eaed7d173f5058d38aef6d7e2ca0e3e0c722059738f430eaa65fc069bcc +size 2036 diff --git a/tests/Images/ReferenceOutput/TextEllipsis_DrawsMaxLinesMarker_210--standard-.png b/tests/Images/ReferenceOutput/TextEllipsis_DrawsMaxLinesMarker_210--standard-.png index f47b246eb..797ac65ff 100644 --- a/tests/Images/ReferenceOutput/TextEllipsis_DrawsMaxLinesMarker_210--standard-.png +++ b/tests/Images/ReferenceOutput/TextEllipsis_DrawsMaxLinesMarker_210--standard-.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ae7d84f044105fd46f2b43d6abbbd36ff93dfc511c30c9a8f4ec192d106bc0fb -size 2218 +oid sha256:30035367303ebbe7a11f082154c52f02868c620006c1d7456e29bcb1c84145f0 +size 2120 diff --git a/tests/Images/ReferenceOutput/TextHyphenation_Custom_DrawsBidiSoftHyphenMarker_109.48-HorizontalBottomTop.png b/tests/Images/ReferenceOutput/TextHyphenation_Custom_DrawsBidiSoftHyphenMarker_109.48-HorizontalBottomTop.png index 813798eba..4d189b9fd 100644 --- a/tests/Images/ReferenceOutput/TextHyphenation_Custom_DrawsBidiSoftHyphenMarker_109.48-HorizontalBottomTop.png +++ b/tests/Images/ReferenceOutput/TextHyphenation_Custom_DrawsBidiSoftHyphenMarker_109.48-HorizontalBottomTop.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eb3d8f7d83bf9ddae45343f82a5e736d330a9677eb16ccc200acd5b45d5df24c -size 7579 +oid sha256:1fa2bf953dcd4af806ded2bbb7203e789ec99b2af91b32e244d1511c6d2c8668 +size 4746 diff --git a/tests/Images/ReferenceOutput/TextHyphenation_Custom_DrawsBidiSoftHyphenMarker_109.48-HorizontalTopBottom.png b/tests/Images/ReferenceOutput/TextHyphenation_Custom_DrawsBidiSoftHyphenMarker_109.48-HorizontalTopBottom.png index fff713126..0c674fc22 100644 --- a/tests/Images/ReferenceOutput/TextHyphenation_Custom_DrawsBidiSoftHyphenMarker_109.48-HorizontalTopBottom.png +++ b/tests/Images/ReferenceOutput/TextHyphenation_Custom_DrawsBidiSoftHyphenMarker_109.48-HorizontalTopBottom.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:94c9bfed10a7670d355b1c0446f8de79470aeedd17d765fd0367b09f22073914 -size 7589 +oid sha256:0f629001a93327c6675a4153f519a6393b1d044f2a35aa31fc36b79ef1449e15 +size 4787 diff --git a/tests/Images/ReferenceOutput/TextHyphenation_Custom_DrawsBidiSoftHyphenMarker_109.48-VerticalMixedLeftRight.png b/tests/Images/ReferenceOutput/TextHyphenation_Custom_DrawsBidiSoftHyphenMarker_109.48-VerticalMixedLeftRight.png index 4ab61e99c..42ade9d9e 100644 --- a/tests/Images/ReferenceOutput/TextHyphenation_Custom_DrawsBidiSoftHyphenMarker_109.48-VerticalMixedLeftRight.png +++ b/tests/Images/ReferenceOutput/TextHyphenation_Custom_DrawsBidiSoftHyphenMarker_109.48-VerticalMixedLeftRight.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:59f861a4c5d809c19a4030ecc6db2e36472d28902bf6ca57b7c324d3be3312e0 -size 7371 +oid sha256:6f53064a22e468bacd73c6138ba15f5557b4dc8a8dbe8bf42778839591766924 +size 4511 diff --git a/tests/Images/ReferenceOutput/TextHyphenation_Custom_DrawsBidiSoftHyphenMarker_109.48-VerticalMixedRightLeft.png b/tests/Images/ReferenceOutput/TextHyphenation_Custom_DrawsBidiSoftHyphenMarker_109.48-VerticalMixedRightLeft.png index 250ce439f..fd6ea03f1 100644 --- a/tests/Images/ReferenceOutput/TextHyphenation_Custom_DrawsBidiSoftHyphenMarker_109.48-VerticalMixedRightLeft.png +++ b/tests/Images/ReferenceOutput/TextHyphenation_Custom_DrawsBidiSoftHyphenMarker_109.48-VerticalMixedRightLeft.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3fbeff7cdfb3bb7436891e7b01c67b483f1a03e213ce9ee469197464ef6056c4 -size 7388 +oid sha256:ada35f11f4e656bdba4f76419a1278f6c0a6a25b5634456060624194530337ea +size 4551 diff --git a/tests/Images/ReferenceOutput/TextHyphenation_Custom_DrawsBidiSoftHyphenMarker_327.76-VerticalLeftRight.png b/tests/Images/ReferenceOutput/TextHyphenation_Custom_DrawsBidiSoftHyphenMarker_327.76-VerticalLeftRight.png index 8ab451fc0..288682d77 100644 --- a/tests/Images/ReferenceOutput/TextHyphenation_Custom_DrawsBidiSoftHyphenMarker_327.76-VerticalLeftRight.png +++ b/tests/Images/ReferenceOutput/TextHyphenation_Custom_DrawsBidiSoftHyphenMarker_327.76-VerticalLeftRight.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fdd4de6215a040115cd19bc2e69deb2f79d6e260f79e3fdca14b89fe220b6b95 -size 9320 +oid sha256:38cafa6993a260c2a8c99f4d9157b8383c84eecf0a345dcad513a4d153a8ac9a +size 4820 diff --git a/tests/Images/ReferenceOutput/TextHyphenation_Custom_DrawsBidiSoftHyphenMarker_327.76-VerticalRightLeft.png b/tests/Images/ReferenceOutput/TextHyphenation_Custom_DrawsBidiSoftHyphenMarker_327.76-VerticalRightLeft.png index 0a2f8eb8c..c98c00a35 100644 --- a/tests/Images/ReferenceOutput/TextHyphenation_Custom_DrawsBidiSoftHyphenMarker_327.76-VerticalRightLeft.png +++ b/tests/Images/ReferenceOutput/TextHyphenation_Custom_DrawsBidiSoftHyphenMarker_327.76-VerticalRightLeft.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:09cdf06c4685c11837b4cd2547093c988e3f2891695a76e21feb9bf44b5b4ff9 -size 9235 +oid sha256:bd95feb86ede3025b7faaa7fc99e7d3166e531c38510d43f01b5ab2868af7546 +size 4779 diff --git a/tests/Images/ReferenceOutput/TextHyphenation_Custom_DrawsSelectedSoftHyphenMarker_246.16-VerticalLeftRight.png b/tests/Images/ReferenceOutput/TextHyphenation_Custom_DrawsSelectedSoftHyphenMarker_246.16-VerticalLeftRight.png index 486413d42..69800a35b 100644 --- a/tests/Images/ReferenceOutput/TextHyphenation_Custom_DrawsSelectedSoftHyphenMarker_246.16-VerticalLeftRight.png +++ b/tests/Images/ReferenceOutput/TextHyphenation_Custom_DrawsSelectedSoftHyphenMarker_246.16-VerticalLeftRight.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e5f08338e26f4a8ca2045540970692f2ac829173bf5214ea16ea3a4e5544703c -size 3326 +oid sha256:0e35aeb6ff85e64817090c9bac3d3b60484b8cafd0f565110e0ef7e9444c5514 +size 3199 diff --git a/tests/Images/ReferenceOutput/TextHyphenation_Custom_DrawsSelectedSoftHyphenMarker_246.16-VerticalRightLeft.png b/tests/Images/ReferenceOutput/TextHyphenation_Custom_DrawsSelectedSoftHyphenMarker_246.16-VerticalRightLeft.png index 37d4aec78..c24050f7a 100644 --- a/tests/Images/ReferenceOutput/TextHyphenation_Custom_DrawsSelectedSoftHyphenMarker_246.16-VerticalRightLeft.png +++ b/tests/Images/ReferenceOutput/TextHyphenation_Custom_DrawsSelectedSoftHyphenMarker_246.16-VerticalRightLeft.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:35ede7344b26f521905d14a53eb820a8279ef86949387a6f5f43b959031b7abe -size 3330 +oid sha256:3d60c6d7d67721412cb1faaf204366353b4f3d205e34f5b45b69c459dbcffe3f +size 3157 diff --git a/tests/Images/ReferenceOutput/TextHyphenation_Custom_DrawsSelectedSoftHyphenMarker_89.17-HorizontalBottomTop.png b/tests/Images/ReferenceOutput/TextHyphenation_Custom_DrawsSelectedSoftHyphenMarker_89.17-HorizontalBottomTop.png index 2aab3e0ce..50054ec9d 100644 --- a/tests/Images/ReferenceOutput/TextHyphenation_Custom_DrawsSelectedSoftHyphenMarker_89.17-HorizontalBottomTop.png +++ b/tests/Images/ReferenceOutput/TextHyphenation_Custom_DrawsSelectedSoftHyphenMarker_89.17-HorizontalBottomTop.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3ee67d6ebbb27a5969544f9d77b9ebb52ef8fa3539ba88850a3293df0c2727b1 -size 3256 +oid sha256:b655ffd4c8da75936bb34c00610b879908c2433753d61f864343c2907096eb33 +size 3152 diff --git a/tests/Images/ReferenceOutput/TextHyphenation_Custom_DrawsSelectedSoftHyphenMarker_89.17-HorizontalTopBottom.png b/tests/Images/ReferenceOutput/TextHyphenation_Custom_DrawsSelectedSoftHyphenMarker_89.17-HorizontalTopBottom.png index 48678123e..b7791783e 100644 --- a/tests/Images/ReferenceOutput/TextHyphenation_Custom_DrawsSelectedSoftHyphenMarker_89.17-HorizontalTopBottom.png +++ b/tests/Images/ReferenceOutput/TextHyphenation_Custom_DrawsSelectedSoftHyphenMarker_89.17-HorizontalTopBottom.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fc8a2ae38a49e634e11b5f6e77e70eb3b977591277cf7a35ac23c275a2f718ff -size 3269 +oid sha256:1e3049495c180ae6f0fad372827843d3f697421a9ba42bc3d7c3854a977f208c +size 3089 diff --git a/tests/Images/ReferenceOutput/TextHyphenation_Custom_DrawsSelectedSoftHyphenMarker_89.17-VerticalMixedLeftRight.png b/tests/Images/ReferenceOutput/TextHyphenation_Custom_DrawsSelectedSoftHyphenMarker_89.17-VerticalMixedLeftRight.png index b5cf667ad..3c8568a55 100644 --- a/tests/Images/ReferenceOutput/TextHyphenation_Custom_DrawsSelectedSoftHyphenMarker_89.17-VerticalMixedLeftRight.png +++ b/tests/Images/ReferenceOutput/TextHyphenation_Custom_DrawsSelectedSoftHyphenMarker_89.17-VerticalMixedLeftRight.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:abd652b0ee5c4b39aa1fe5795cabb3fc36f3b33a218765c2e2721a8c6fd0e882 -size 3061 +oid sha256:a0fb0558975a3b81a93e5a81765e53ec21fbe4476bfdf1b895c44a4cddf5dfcb +size 2957 diff --git a/tests/Images/ReferenceOutput/TextHyphenation_Custom_DrawsSelectedSoftHyphenMarker_89.17-VerticalMixedRightLeft.png b/tests/Images/ReferenceOutput/TextHyphenation_Custom_DrawsSelectedSoftHyphenMarker_89.17-VerticalMixedRightLeft.png index 232bf31d8..ba3a2b9e2 100644 --- a/tests/Images/ReferenceOutput/TextHyphenation_Custom_DrawsSelectedSoftHyphenMarker_89.17-VerticalMixedRightLeft.png +++ b/tests/Images/ReferenceOutput/TextHyphenation_Custom_DrawsSelectedSoftHyphenMarker_89.17-VerticalMixedRightLeft.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d6ab1466e84298a798d8ba92f64b5a2081736f0367058a94be0a19ef19ce16fd -size 3063 +oid sha256:66e9ab261d068cda0a61429a2edbfbb942890cd46eb7f0af1e3a9d66d3804779 +size 2956 diff --git a/tests/Images/ReferenceOutput/TextHyphenation_None_DoesNotDrawFallbackSoftHyphenMarker_100.48-HorizontalBottomTop.png b/tests/Images/ReferenceOutput/TextHyphenation_None_DoesNotDrawFallbackSoftHyphenMarker_100.48-HorizontalBottomTop.png index 9c9b9896f..694acb1e0 100644 --- a/tests/Images/ReferenceOutput/TextHyphenation_None_DoesNotDrawFallbackSoftHyphenMarker_100.48-HorizontalBottomTop.png +++ b/tests/Images/ReferenceOutput/TextHyphenation_None_DoesNotDrawFallbackSoftHyphenMarker_100.48-HorizontalBottomTop.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:285c313b76a694bba7901649efd9b2d0d671fa0002f3d22b3e169ba1826a7876 -size 4322 +oid sha256:6ea431e5a2113aaf2a6893cbd406b9d55128c3e82a3be369d2a3b863e7a74a4a +size 4174 diff --git a/tests/Images/ReferenceOutput/TextHyphenation_None_DoesNotDrawFallbackSoftHyphenMarker_100.48-HorizontalTopBottom.png b/tests/Images/ReferenceOutput/TextHyphenation_None_DoesNotDrawFallbackSoftHyphenMarker_100.48-HorizontalTopBottom.png index ba619ae4e..8e530f08c 100644 --- a/tests/Images/ReferenceOutput/TextHyphenation_None_DoesNotDrawFallbackSoftHyphenMarker_100.48-HorizontalTopBottom.png +++ b/tests/Images/ReferenceOutput/TextHyphenation_None_DoesNotDrawFallbackSoftHyphenMarker_100.48-HorizontalTopBottom.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:48352df0e174529ee613230e3fb613014899c8e1ad308e57ede0f7a4f3e3a193 -size 4302 +oid sha256:473b3f210b3949834c028d36fec5bc109adbc65c0e9624516bee503602c83b6c +size 4212 diff --git a/tests/Images/ReferenceOutput/TextHyphenation_None_DoesNotDrawFallbackSoftHyphenMarker_100.48-VerticalMixedLeftRight.png b/tests/Images/ReferenceOutput/TextHyphenation_None_DoesNotDrawFallbackSoftHyphenMarker_100.48-VerticalMixedLeftRight.png index 500817b90..6de34de06 100644 --- a/tests/Images/ReferenceOutput/TextHyphenation_None_DoesNotDrawFallbackSoftHyphenMarker_100.48-VerticalMixedLeftRight.png +++ b/tests/Images/ReferenceOutput/TextHyphenation_None_DoesNotDrawFallbackSoftHyphenMarker_100.48-VerticalMixedLeftRight.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ee4d947ea13c46e8559b45c7ce64338ff642ab44325c6b59864ceee4f9eb3830 -size 4122 +oid sha256:0bc8e357139e2cc492c71bc228adb7d8e7f86529da3c39092ca6f9c88132a401 +size 4038 diff --git a/tests/Images/ReferenceOutput/TextHyphenation_None_DoesNotDrawFallbackSoftHyphenMarker_100.48-VerticalMixedRightLeft.png b/tests/Images/ReferenceOutput/TextHyphenation_None_DoesNotDrawFallbackSoftHyphenMarker_100.48-VerticalMixedRightLeft.png index 3ca6afb16..a3e3258ef 100644 --- a/tests/Images/ReferenceOutput/TextHyphenation_None_DoesNotDrawFallbackSoftHyphenMarker_100.48-VerticalMixedRightLeft.png +++ b/tests/Images/ReferenceOutput/TextHyphenation_None_DoesNotDrawFallbackSoftHyphenMarker_100.48-VerticalMixedRightLeft.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c76ecfb163c8452d9b619ddbfcef183c1eb4c0f97cf37791b4d58fb80ef4785b -size 4088 +oid sha256:a7a8c77383b25061d7450c237c4804c7d50bf90c9002a4e352d7c0e6a051b3ff +size 4003 diff --git a/tests/Images/ReferenceOutput/TextHyphenation_None_DoesNotDrawFallbackSoftHyphenMarker_326.19998-VerticalLeftRight.png b/tests/Images/ReferenceOutput/TextHyphenation_None_DoesNotDrawFallbackSoftHyphenMarker_326.19998-VerticalLeftRight.png index 0f4449375..a30a79bc4 100644 --- a/tests/Images/ReferenceOutput/TextHyphenation_None_DoesNotDrawFallbackSoftHyphenMarker_326.19998-VerticalLeftRight.png +++ b/tests/Images/ReferenceOutput/TextHyphenation_None_DoesNotDrawFallbackSoftHyphenMarker_326.19998-VerticalLeftRight.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4ba48eb5f7b8efe76750f0a1a8e730e0d5eb866f64f6be0d67287505ee081626 -size 4419 +oid sha256:3b630192598cf6de705d6869d454cf86b96f5b7e12dcbb3048127cd411339451 +size 4231 diff --git a/tests/Images/ReferenceOutput/TextHyphenation_None_DoesNotDrawFallbackSoftHyphenMarker_326.19998-VerticalRightLeft.png b/tests/Images/ReferenceOutput/TextHyphenation_None_DoesNotDrawFallbackSoftHyphenMarker_326.19998-VerticalRightLeft.png index 63246a6a4..6f622bc98 100644 --- a/tests/Images/ReferenceOutput/TextHyphenation_None_DoesNotDrawFallbackSoftHyphenMarker_326.19998-VerticalRightLeft.png +++ b/tests/Images/ReferenceOutput/TextHyphenation_None_DoesNotDrawFallbackSoftHyphenMarker_326.19998-VerticalRightLeft.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8eef7075f28b87e21a1030f75048c92c3b2cea0a63a045b295d6b8a6a7a582b5 -size 4395 +oid sha256:480b2ef206eb43fd6a9a7d43ea3d15efb65a844fd623af1f9a535e5309b5691c +size 4251 diff --git a/tests/Images/ReferenceOutput/TextHyphenation_Standard_DrawsFallbackSoftHyphenMarker_101.61001-HorizontalBottomTop.png b/tests/Images/ReferenceOutput/TextHyphenation_Standard_DrawsFallbackSoftHyphenMarker_101.61001-HorizontalBottomTop.png index 49c63eeaf..202a43526 100644 --- a/tests/Images/ReferenceOutput/TextHyphenation_Standard_DrawsFallbackSoftHyphenMarker_101.61001-HorizontalBottomTop.png +++ b/tests/Images/ReferenceOutput/TextHyphenation_Standard_DrawsFallbackSoftHyphenMarker_101.61001-HorizontalBottomTop.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:edaafc8577d613aad1f87ff8587533b7648af61ba87a89d888293a2983f5b791 -size 6393 +oid sha256:81ddb5861df86a924fe364b1642ea30085eda489a81683de9c5b6cd93edde8eb +size 4249 diff --git a/tests/Images/ReferenceOutput/TextHyphenation_Standard_DrawsFallbackSoftHyphenMarker_101.61001-HorizontalTopBottom.png b/tests/Images/ReferenceOutput/TextHyphenation_Standard_DrawsFallbackSoftHyphenMarker_101.61001-HorizontalTopBottom.png index 7b93f436a..dabd63215 100644 --- a/tests/Images/ReferenceOutput/TextHyphenation_Standard_DrawsFallbackSoftHyphenMarker_101.61001-HorizontalTopBottom.png +++ b/tests/Images/ReferenceOutput/TextHyphenation_Standard_DrawsFallbackSoftHyphenMarker_101.61001-HorizontalTopBottom.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0941441973cd552c564090df4546c706a699210f683c1208b0a2e8f7eb47627f -size 4217 +oid sha256:ccca20e836bd5dda6b7a88d85c85439c4618a8172d7976613cd7920c35c995d5 +size 4232 diff --git a/tests/Images/ReferenceOutput/TextHyphenation_Standard_DrawsFallbackSoftHyphenMarker_101.61001-VerticalMixedLeftRight.png b/tests/Images/ReferenceOutput/TextHyphenation_Standard_DrawsFallbackSoftHyphenMarker_101.61001-VerticalMixedLeftRight.png index ab7cbceb7..07138b0b0 100644 --- a/tests/Images/ReferenceOutput/TextHyphenation_Standard_DrawsFallbackSoftHyphenMarker_101.61001-VerticalMixedLeftRight.png +++ b/tests/Images/ReferenceOutput/TextHyphenation_Standard_DrawsFallbackSoftHyphenMarker_101.61001-VerticalMixedLeftRight.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:85c8497490d814b2a6bfb2043d4b71a70b8b9c6195c45275995d3364c32029f7 +oid sha256:9821c834dc11b3fdbe284de0e891ce77583bbc36e2698c7df3d6ff71981287e0 size 4041 diff --git a/tests/Images/ReferenceOutput/TextHyphenation_Standard_DrawsFallbackSoftHyphenMarker_101.61001-VerticalMixedRightLeft.png b/tests/Images/ReferenceOutput/TextHyphenation_Standard_DrawsFallbackSoftHyphenMarker_101.61001-VerticalMixedRightLeft.png index d8b17a445..9fa1d5cc8 100644 --- a/tests/Images/ReferenceOutput/TextHyphenation_Standard_DrawsFallbackSoftHyphenMarker_101.61001-VerticalMixedRightLeft.png +++ b/tests/Images/ReferenceOutput/TextHyphenation_Standard_DrawsFallbackSoftHyphenMarker_101.61001-VerticalMixedRightLeft.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a844d13380fd23008a2deb130237d3d3e753c8d77d9e3ee8cce3242bf287f91a -size 6483 +oid sha256:cc273f8c0c02fcc62fcaa20cc802a9fdcd22720900c999a9e4099bb35fa3fdae +size 4038 diff --git a/tests/Images/ReferenceOutput/TextHyphenation_Standard_DrawsFallbackSoftHyphenMarker_326.76-VerticalLeftRight.png b/tests/Images/ReferenceOutput/TextHyphenation_Standard_DrawsFallbackSoftHyphenMarker_326.76-VerticalLeftRight.png index a41a6d086..b29cf0a22 100644 --- a/tests/Images/ReferenceOutput/TextHyphenation_Standard_DrawsFallbackSoftHyphenMarker_326.76-VerticalLeftRight.png +++ b/tests/Images/ReferenceOutput/TextHyphenation_Standard_DrawsFallbackSoftHyphenMarker_326.76-VerticalLeftRight.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0bfd91d6847a8ed14a65d9be4ad54d86821c5a274f0fd10b5ff27213f48b6d22 -size 4459 +oid sha256:59adfb58848c64cb971c61be366099d995142b9854e2e7cd46b2e46010f5a25d +size 4282 diff --git a/tests/Images/ReferenceOutput/TextHyphenation_Standard_DrawsFallbackSoftHyphenMarker_326.76-VerticalRightLeft.png b/tests/Images/ReferenceOutput/TextHyphenation_Standard_DrawsFallbackSoftHyphenMarker_326.76-VerticalRightLeft.png index ec899771a..08f605b49 100644 --- a/tests/Images/ReferenceOutput/TextHyphenation_Standard_DrawsFallbackSoftHyphenMarker_326.76-VerticalRightLeft.png +++ b/tests/Images/ReferenceOutput/TextHyphenation_Standard_DrawsFallbackSoftHyphenMarker_326.76-VerticalRightLeft.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:03ed837ae1d4e1323c93d5dce192a6b537ec220d05a9aca194431efe2ece6535 -size 4426 +oid sha256:d50731477c04d505d712e7223196ade431258a64638e342f123e8b15e84e43ee +size 4259 diff --git a/tests/Images/ReferenceOutput/TextJustification_InterCharacter_Horizontal_400-_rtl_False_.png b/tests/Images/ReferenceOutput/TextJustification_InterCharacter_Horizontal_400-_rtl_False_.png index 951624308..b6bcc8662 100644 --- a/tests/Images/ReferenceOutput/TextJustification_InterCharacter_Horizontal_400-_rtl_False_.png +++ b/tests/Images/ReferenceOutput/TextJustification_InterCharacter_Horizontal_400-_rtl_False_.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ccab71a2b43575865b3b3a7ace9206aca07d9499a451b6a2bb0fcb59af185ae1 -size 8631 +oid sha256:e9454d0eef7f97e1ad9162709a60cae2883b58da7507bb5bc53c69f2defe3cac +size 7614 diff --git a/tests/Images/ReferenceOutput/TextJustification_InterCharacter_Horizontal_400-_rtl_True_.png b/tests/Images/ReferenceOutput/TextJustification_InterCharacter_Horizontal_400-_rtl_True_.png index eb5ce6c39..d1257f9cf 100644 --- a/tests/Images/ReferenceOutput/TextJustification_InterCharacter_Horizontal_400-_rtl_True_.png +++ b/tests/Images/ReferenceOutput/TextJustification_InterCharacter_Horizontal_400-_rtl_True_.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cabafb2303f79b345c9b8b81886f819dbff6caed75598028081b9733c5651e54 -size 8550 +oid sha256:7fb3ea08aafd4ed73b08746dffb055a8e02b31ac899b31da257a08fb8a1eeb95 +size 7588 diff --git a/tests/Images/ReferenceOutput/TextJustification_InterCharacter_Vertical_400-_rtl_False_.png b/tests/Images/ReferenceOutput/TextJustification_InterCharacter_Vertical_400-_rtl_False_.png index 67c2e05ad..0791894c3 100644 --- a/tests/Images/ReferenceOutput/TextJustification_InterCharacter_Vertical_400-_rtl_False_.png +++ b/tests/Images/ReferenceOutput/TextJustification_InterCharacter_Vertical_400-_rtl_False_.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4a42a5c0284bbe62b955b83752e15fe89408aa99b6a05557c78e47e78e9ed804 -size 7483 +oid sha256:2c1e379db45a5ea33ff9d32b5dc66a391c8554dfaa39bad52e1cf21bbd989320 +size 8165 diff --git a/tests/Images/ReferenceOutput/TextJustification_InterCharacter_Vertical_400-_rtl_True_.png b/tests/Images/ReferenceOutput/TextJustification_InterCharacter_Vertical_400-_rtl_True_.png index e1bd2f06f..38e3a74ec 100644 --- a/tests/Images/ReferenceOutput/TextJustification_InterCharacter_Vertical_400-_rtl_True_.png +++ b/tests/Images/ReferenceOutput/TextJustification_InterCharacter_Vertical_400-_rtl_True_.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b8f74f86462231d5f070f28563b5b2374550d5419a788bf7cd2391dad998d7a0 -size 7411 +oid sha256:fce8ffcb3efbdd795330f032ce670f66d3aed6c07482391f973c45bf5c4a9779 +size 8160 diff --git a/tests/Images/ReferenceOutput/TextJustification_InterWord_Horizontal_400-_rtl_False_.png b/tests/Images/ReferenceOutput/TextJustification_InterWord_Horizontal_400-_rtl_False_.png index cf36a71e6..8d32d2628 100644 --- a/tests/Images/ReferenceOutput/TextJustification_InterWord_Horizontal_400-_rtl_False_.png +++ b/tests/Images/ReferenceOutput/TextJustification_InterWord_Horizontal_400-_rtl_False_.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d62dfb98ea4731756395921671a90a3d517735ad79710c0b6d767b428c33138b -size 6432 +oid sha256:5359c8c17c263e526703d1c212530fd6292a535b471ef91dcee703164f86014e +size 7583 diff --git a/tests/Images/ReferenceOutput/TextJustification_InterWord_Horizontal_400-_rtl_True_.png b/tests/Images/ReferenceOutput/TextJustification_InterWord_Horizontal_400-_rtl_True_.png index 0bd2eda27..26ff5a9ad 100644 --- a/tests/Images/ReferenceOutput/TextJustification_InterWord_Horizontal_400-_rtl_True_.png +++ b/tests/Images/ReferenceOutput/TextJustification_InterWord_Horizontal_400-_rtl_True_.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:93d5934e78aa0660817f713fdc153b760a6157242f919b3db193cce757b091f6 -size 6458 +oid sha256:26fe76ec96878965488df4a85a875e69ab2149d25fb2155e4f56567ed9752583 +size 7549 diff --git a/tests/Images/ReferenceOutput/TextJustification_InterWord_Vertical_400-_rtl_False_.png b/tests/Images/ReferenceOutput/TextJustification_InterWord_Vertical_400-_rtl_False_.png index 73a7f087a..bc823626e 100644 --- a/tests/Images/ReferenceOutput/TextJustification_InterWord_Vertical_400-_rtl_False_.png +++ b/tests/Images/ReferenceOutput/TextJustification_InterWord_Vertical_400-_rtl_False_.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ebb45b4fc33003cbec06666595727292fc61729e9b62d27d4eaa335c2b5b209a -size 6825 +oid sha256:18140f203b745d91a274d949cbccdf5f4bf3549db9c8723b9db1a85978314a8b +size 7723 diff --git a/tests/Images/ReferenceOutput/TextJustification_InterWord_Vertical_400-_rtl_True_.png b/tests/Images/ReferenceOutput/TextJustification_InterWord_Vertical_400-_rtl_True_.png index 33c39aad7..f3faf30ba 100644 --- a/tests/Images/ReferenceOutput/TextJustification_InterWord_Vertical_400-_rtl_True_.png +++ b/tests/Images/ReferenceOutput/TextJustification_InterWord_Vertical_400-_rtl_True_.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ffc44fd5b6a78c391ef1f864c4dbfd79381d77d429cb13b02296a8ccd539d3a9 -size 6779 +oid sha256:a32349728658335469316c68ae6c27521c2fc18dab28d369dd2b847741206198 +size 7674 diff --git a/tests/Images/ReferenceOutput/TextJustification_MultiParagraph_Horizontal_SkipsFinalLines_400-_rtl_False-mode_InterCharacter_.png b/tests/Images/ReferenceOutput/TextJustification_MultiParagraph_Horizontal_SkipsFinalLines_400-_rtl_False-mode_InterCharacter_.png index c9b5f4963..c711ee260 100644 --- a/tests/Images/ReferenceOutput/TextJustification_MultiParagraph_Horizontal_SkipsFinalLines_400-_rtl_False-mode_InterCharacter_.png +++ b/tests/Images/ReferenceOutput/TextJustification_MultiParagraph_Horizontal_SkipsFinalLines_400-_rtl_False-mode_InterCharacter_.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3592f8f99920524ed105c0f9a305c1232480c2bd736e3c345cfdc306f100eb79 -size 15865 +oid sha256:826fc3b9acd5b6ac4911a55bc57cf68e51f20b0f6b273bcc84802a762d790255 +size 8238 diff --git a/tests/Images/ReferenceOutput/TextJustification_MultiParagraph_Horizontal_SkipsFinalLines_400-_rtl_False-mode_InterWord_.png b/tests/Images/ReferenceOutput/TextJustification_MultiParagraph_Horizontal_SkipsFinalLines_400-_rtl_False-mode_InterWord_.png index 436cecd33..9e61354c6 100644 --- a/tests/Images/ReferenceOutput/TextJustification_MultiParagraph_Horizontal_SkipsFinalLines_400-_rtl_False-mode_InterWord_.png +++ b/tests/Images/ReferenceOutput/TextJustification_MultiParagraph_Horizontal_SkipsFinalLines_400-_rtl_False-mode_InterWord_.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:44e254015190576975527548c20663b13951fae696526c91913a02646efe133a -size 6681 +oid sha256:4d393cd13ebe53fab513056c78f4b1fd5e10c3d068580f52a83eb3f51e3344cb +size 8335 diff --git a/tests/Images/ReferenceOutput/TextJustification_MultiParagraph_Horizontal_SkipsFinalLines_400-_rtl_True-mode_InterCharacter_.png b/tests/Images/ReferenceOutput/TextJustification_MultiParagraph_Horizontal_SkipsFinalLines_400-_rtl_True-mode_InterCharacter_.png index 95399fcdc..96e9fc1fa 100644 --- a/tests/Images/ReferenceOutput/TextJustification_MultiParagraph_Horizontal_SkipsFinalLines_400-_rtl_True-mode_InterCharacter_.png +++ b/tests/Images/ReferenceOutput/TextJustification_MultiParagraph_Horizontal_SkipsFinalLines_400-_rtl_True-mode_InterCharacter_.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:342c26e16e91b4d004cc6cd06ae46edb9934f67321d4834bfe93e07dbe008224 -size 15799 +oid sha256:7e18dac17d25850dc62da8cae72015fb48a28e3fbde0f21f99379f353e02c369 +size 8975 diff --git a/tests/Images/ReferenceOutput/TextJustification_MultiParagraph_Horizontal_SkipsFinalLines_400-_rtl_True-mode_InterWord_.png b/tests/Images/ReferenceOutput/TextJustification_MultiParagraph_Horizontal_SkipsFinalLines_400-_rtl_True-mode_InterWord_.png index a362ce28e..af96d8a5e 100644 --- a/tests/Images/ReferenceOutput/TextJustification_MultiParagraph_Horizontal_SkipsFinalLines_400-_rtl_True-mode_InterWord_.png +++ b/tests/Images/ReferenceOutput/TextJustification_MultiParagraph_Horizontal_SkipsFinalLines_400-_rtl_True-mode_InterWord_.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bea37c9b9a3552bb92b21262ca62f67d643528a6c79381fbae210c63d6330871 -size 16048 +oid sha256:18eb37fcd6bd7bea868596994a74f02e4688587cf389e4a5ac69ebb553f6fe6e +size 8982 diff --git a/tests/Images/ReferenceOutput/TextJustification_MultiParagraph_Vertical_SkipsFinalLines_400-_rtl_False-mode_InterCharacter_.png b/tests/Images/ReferenceOutput/TextJustification_MultiParagraph_Vertical_SkipsFinalLines_400-_rtl_False-mode_InterCharacter_.png index 89150e3ab..03b766c22 100644 --- a/tests/Images/ReferenceOutput/TextJustification_MultiParagraph_Vertical_SkipsFinalLines_400-_rtl_False-mode_InterCharacter_.png +++ b/tests/Images/ReferenceOutput/TextJustification_MultiParagraph_Vertical_SkipsFinalLines_400-_rtl_False-mode_InterCharacter_.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:83b94cb32043cafa7d9f128070cf27c8fd1f8b5bdb89bcb55022965b45cea9aa -size 8931 +oid sha256:9cb2eb5b5010eb12d4b38f52c0a6a0ee402bad30881d1e00aaf7896530208a8f +size 10005 diff --git a/tests/Images/ReferenceOutput/TextJustification_MultiParagraph_Vertical_SkipsFinalLines_400-_rtl_False-mode_InterWord_.png b/tests/Images/ReferenceOutput/TextJustification_MultiParagraph_Vertical_SkipsFinalLines_400-_rtl_False-mode_InterWord_.png index 8f5832e57..0930384a3 100644 --- a/tests/Images/ReferenceOutput/TextJustification_MultiParagraph_Vertical_SkipsFinalLines_400-_rtl_False-mode_InterWord_.png +++ b/tests/Images/ReferenceOutput/TextJustification_MultiParagraph_Vertical_SkipsFinalLines_400-_rtl_False-mode_InterWord_.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a474ccbf6d725577897f116f7aad5fec54d4899e2c8e76f4a99eb47c99c8dc9d -size 8200 +oid sha256:d3a656b103ae0ca8e3ff1f39ee1dc28be134b1495fced41acfe354f583cf73ae +size 9496 diff --git a/tests/Images/ReferenceOutput/TextJustification_MultiParagraph_Vertical_SkipsFinalLines_400-_rtl_True-mode_InterCharacter_.png b/tests/Images/ReferenceOutput/TextJustification_MultiParagraph_Vertical_SkipsFinalLines_400-_rtl_True-mode_InterCharacter_.png index 320c9fd03..0998478ab 100644 --- a/tests/Images/ReferenceOutput/TextJustification_MultiParagraph_Vertical_SkipsFinalLines_400-_rtl_True-mode_InterCharacter_.png +++ b/tests/Images/ReferenceOutput/TextJustification_MultiParagraph_Vertical_SkipsFinalLines_400-_rtl_True-mode_InterCharacter_.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:54ad0ef97bf4f7af758f8ce6bb33752bd09b7cdb19f6abaaed956ca9ce54b4c1 -size 9441 +oid sha256:e5a42607b4ca5dc152620f229c862bd105f382e0a5568035d9ff03b39fde020a +size 10619 diff --git a/tests/Images/ReferenceOutput/TextJustification_MultiParagraph_Vertical_SkipsFinalLines_400-_rtl_True-mode_InterWord_.png b/tests/Images/ReferenceOutput/TextJustification_MultiParagraph_Vertical_SkipsFinalLines_400-_rtl_True-mode_InterWord_.png index 88444c87a..5c6498586 100644 --- a/tests/Images/ReferenceOutput/TextJustification_MultiParagraph_Vertical_SkipsFinalLines_400-_rtl_True-mode_InterWord_.png +++ b/tests/Images/ReferenceOutput/TextJustification_MultiParagraph_Vertical_SkipsFinalLines_400-_rtl_True-mode_InterWord_.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:007de9501bfb98d4feff311f9251e0e07182a78a59f9478f46340cb5023b86aa -size 8568 +oid sha256:01d4bb655904bb1c4c78ec46c9c83768eb37bf648c4716caa8dda5a3542d60f1 +size 10071 diff --git a/tests/Images/ReferenceOutput/TextPlaceholder_DrawsInlineReservedSpace_-abovebaseline-.png b/tests/Images/ReferenceOutput/TextPlaceholder_DrawsInlineReservedSpace_-abovebaseline-.png index 980e0b7b4..b7a4b09a0 100644 --- a/tests/Images/ReferenceOutput/TextPlaceholder_DrawsInlineReservedSpace_-abovebaseline-.png +++ b/tests/Images/ReferenceOutput/TextPlaceholder_DrawsInlineReservedSpace_-abovebaseline-.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eefe9e903ce819e67e4ae1e13b90f2cc03876b338da7ce47b56cc549c1c623e6 -size 7653 +oid sha256:f9ff80861ea15d93b61d6d0beb57dd6c7c8eba338d56fc1a1e6f98c5dbc0cb38 +size 7873 diff --git a/tests/Images/ReferenceOutput/TextPlaceholder_DrawsInlineReservedSpace_-baseline-.png b/tests/Images/ReferenceOutput/TextPlaceholder_DrawsInlineReservedSpace_-baseline-.png index b9611263d..c6a03bc8c 100644 --- a/tests/Images/ReferenceOutput/TextPlaceholder_DrawsInlineReservedSpace_-baseline-.png +++ b/tests/Images/ReferenceOutput/TextPlaceholder_DrawsInlineReservedSpace_-baseline-.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0c745aba3efac23b87df347c3f5a5a4264a3458766ccad2e5faf380b560152e2 -size 7702 +oid sha256:90aff0fecbe32b5410f1ba8db09a3cb1cb92a641399d385ccefc2cfe021bb2bc +size 7898 diff --git a/tests/Images/ReferenceOutput/TextPlaceholder_DrawsInlineReservedSpace_-belowbaseline-.png b/tests/Images/ReferenceOutput/TextPlaceholder_DrawsInlineReservedSpace_-belowbaseline-.png index 1cfe20c56..0f5275467 100644 --- a/tests/Images/ReferenceOutput/TextPlaceholder_DrawsInlineReservedSpace_-belowbaseline-.png +++ b/tests/Images/ReferenceOutput/TextPlaceholder_DrawsInlineReservedSpace_-belowbaseline-.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:536bb4efd4e0a36a520093c49aaef594a60b31f346afc56d111a59ba487e0ce0 -size 7833 +oid sha256:4fd610064cf9c02b4aba450b6378693adcf736fc457a07d57eddf1d09e7f3c89 +size 8022 diff --git a/tests/Images/ReferenceOutput/TextPlaceholder_DrawsInlineReservedSpace_-bottom-.png b/tests/Images/ReferenceOutput/TextPlaceholder_DrawsInlineReservedSpace_-bottom-.png index 689f50f5c..5c5bdfdc6 100644 --- a/tests/Images/ReferenceOutput/TextPlaceholder_DrawsInlineReservedSpace_-bottom-.png +++ b/tests/Images/ReferenceOutput/TextPlaceholder_DrawsInlineReservedSpace_-bottom-.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:45c2ef53f30308fd02f610f70ed4dcdfc133644ccac54593f7554cebce804a5e -size 7648 +oid sha256:18728e98809d47df20b5f2203590f7f68e956099a000f2262040c81a1cec515f +size 7856 diff --git a/tests/Images/ReferenceOutput/TextPlaceholder_DrawsInlineReservedSpace_-middle-.png b/tests/Images/ReferenceOutput/TextPlaceholder_DrawsInlineReservedSpace_-middle-.png index 6d4b899b1..68a082ab0 100644 --- a/tests/Images/ReferenceOutput/TextPlaceholder_DrawsInlineReservedSpace_-middle-.png +++ b/tests/Images/ReferenceOutput/TextPlaceholder_DrawsInlineReservedSpace_-middle-.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b6d7b060fb884e340e8ec92a99b0bb8d712bf3e70b6c5fb5e757cf49c0d5eb88 -size 7640 +oid sha256:c0397c629ba1e220de56adfd9f66911ece8cb422b62364d439c3ea399146a88a +size 7852 diff --git a/tests/Images/ReferenceOutput/TextPlaceholder_DrawsInlineReservedSpace_-top-.png b/tests/Images/ReferenceOutput/TextPlaceholder_DrawsInlineReservedSpace_-top-.png index 75d552708..9c814edb6 100644 --- a/tests/Images/ReferenceOutput/TextPlaceholder_DrawsInlineReservedSpace_-top-.png +++ b/tests/Images/ReferenceOutput/TextPlaceholder_DrawsInlineReservedSpace_-top-.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:93cf164e3b6a139ff559d46a46cfd869d02b78bfc405399bbd60639f2eccf81f -size 7630 +oid sha256:8c98aae1ac9823213d84069793579f6010ebc23371f91ea990d8bd47e4272c10 +size 7874 diff --git a/tests/Images/ReferenceOutput/TextPlaceholder_DrawsOversizedInlineReservedSpace_-abovebaseline-.png b/tests/Images/ReferenceOutput/TextPlaceholder_DrawsOversizedInlineReservedSpace_-abovebaseline-.png index 9413ab43a..e248348a0 100644 --- a/tests/Images/ReferenceOutput/TextPlaceholder_DrawsOversizedInlineReservedSpace_-abovebaseline-.png +++ b/tests/Images/ReferenceOutput/TextPlaceholder_DrawsOversizedInlineReservedSpace_-abovebaseline-.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1979a96d6f621f97e2f5b642cc4fed8a61c1d4a8e940791c562ee3b756fd5794 -size 7996 +oid sha256:4c60727ca650cb42ca4ed1d5cfdbd6f81564cc5248143eb7a2ebccb9d5ed7294 +size 8074 diff --git a/tests/Images/ReferenceOutput/TextPlaceholder_DrawsOversizedInlineReservedSpace_-baseline-.png b/tests/Images/ReferenceOutput/TextPlaceholder_DrawsOversizedInlineReservedSpace_-baseline-.png index 847564b6c..99bd09591 100644 --- a/tests/Images/ReferenceOutput/TextPlaceholder_DrawsOversizedInlineReservedSpace_-baseline-.png +++ b/tests/Images/ReferenceOutput/TextPlaceholder_DrawsOversizedInlineReservedSpace_-baseline-.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7aee026bee1b2e42414b3ce030d6aae53e72c19714a396809a697acbc1b34f5a -size 8220 +oid sha256:b6905f4a30362048c47959b58f812ec658d424b81f8cecddf3f54c78a64090d2 +size 8479 diff --git a/tests/Images/ReferenceOutput/TextPlaceholder_DrawsOversizedInlineReservedSpace_-belowbaseline-.png b/tests/Images/ReferenceOutput/TextPlaceholder_DrawsOversizedInlineReservedSpace_-belowbaseline-.png index 58d9255e9..7fc20a962 100644 --- a/tests/Images/ReferenceOutput/TextPlaceholder_DrawsOversizedInlineReservedSpace_-belowbaseline-.png +++ b/tests/Images/ReferenceOutput/TextPlaceholder_DrawsOversizedInlineReservedSpace_-belowbaseline-.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:081b880ac41fa0db308e7fdbe1cfb9467cc3a1d9c8f48a4f9b622fcbe87c891a -size 8222 +oid sha256:efabc3be3dfa75469229cbd4287c1c7b416368f6db30740d8b1602fbf217415f +size 8486 diff --git a/tests/Images/ReferenceOutput/TextPlaceholder_DrawsOversizedInlineReservedSpace_-bottom-.png b/tests/Images/ReferenceOutput/TextPlaceholder_DrawsOversizedInlineReservedSpace_-bottom-.png index b254e6712..511a336f2 100644 --- a/tests/Images/ReferenceOutput/TextPlaceholder_DrawsOversizedInlineReservedSpace_-bottom-.png +++ b/tests/Images/ReferenceOutput/TextPlaceholder_DrawsOversizedInlineReservedSpace_-bottom-.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a774eb0e454b8c2408481b2f5fd8da5dcf43d47d1fb55ec8f32e3cb61d6dd6f4 -size 7980 +oid sha256:7df5c5d619aaad329759ab217b3412d74c7ab684547adea4440da036d6f1b53e +size 8032 diff --git a/tests/Images/ReferenceOutput/TextPlaceholder_DrawsOversizedInlineReservedSpace_-middle-.png b/tests/Images/ReferenceOutput/TextPlaceholder_DrawsOversizedInlineReservedSpace_-middle-.png index 313a25e35..7b2582a84 100644 --- a/tests/Images/ReferenceOutput/TextPlaceholder_DrawsOversizedInlineReservedSpace_-middle-.png +++ b/tests/Images/ReferenceOutput/TextPlaceholder_DrawsOversizedInlineReservedSpace_-middle-.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:437de571bec3778a4354c8c0519e4e1a8bee92a352b1c0c29ee8a4b99df8762a -size 8267 +oid sha256:eabdd28ac8c848555bf75e8616b1678a17b95c0f09157cccfb7d43b18878aab1 +size 8316 diff --git a/tests/Images/ReferenceOutput/TextPlaceholder_DrawsOversizedInlineReservedSpace_-top-.png b/tests/Images/ReferenceOutput/TextPlaceholder_DrawsOversizedInlineReservedSpace_-top-.png index 6ccebecfc..d3b0a3f4b 100644 --- a/tests/Images/ReferenceOutput/TextPlaceholder_DrawsOversizedInlineReservedSpace_-top-.png +++ b/tests/Images/ReferenceOutput/TextPlaceholder_DrawsOversizedInlineReservedSpace_-top-.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c06268976e3216feb9f1a5987efb049c97c0856cad54c655695d8152fe5a7f6b -size 8184 +oid sha256:f25455d3ac76f7a2d0a26420be0453971eb589e2eba2649a2f67c801db7872a4 +size 8421 diff --git a/tests/Images/ReferenceOutput/VisualTest_AdobeVFPrototype_GVar_WeightVariations_-Black--900.png b/tests/Images/ReferenceOutput/VisualTest_AdobeVFPrototype_GVar_WeightVariations_-Black--900.png index 86fad1893..f2f8ecf30 100644 --- a/tests/Images/ReferenceOutput/VisualTest_AdobeVFPrototype_GVar_WeightVariations_-Black--900.png +++ b/tests/Images/ReferenceOutput/VisualTest_AdobeVFPrototype_GVar_WeightVariations_-Black--900.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:931c83067acb0c535450f1a952c92bb06aa01c267ef654c9c014980dde2b0ff2 -size 2424 +oid sha256:5e3211dc45144df0b519a91c27952824c1744b37d1adf6b81f11fe54c5bd6394 +size 2428 diff --git a/tests/Images/ReferenceOutput/VisualTest_AdobeVFPrototype_GVar_WeightVariations_-Light--200.png b/tests/Images/ReferenceOutput/VisualTest_AdobeVFPrototype_GVar_WeightVariations_-Light--200.png index 6143ae9bf..ed3329f5e 100644 --- a/tests/Images/ReferenceOutput/VisualTest_AdobeVFPrototype_GVar_WeightVariations_-Light--200.png +++ b/tests/Images/ReferenceOutput/VisualTest_AdobeVFPrototype_GVar_WeightVariations_-Light--200.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d08a505bbd75b0f3586604b202e432ec55e3d1cac8055edb28f3998c600af37e -size 2207 +oid sha256:67756a126690c1526a2f4f67f3a11471ba4efbbf1d7cd73d38d4526e528c8b79 +size 2259 diff --git a/tests/Images/ReferenceOutput/VisualTest_NotoEmoji_WeightVariations_-Bold--700.png b/tests/Images/ReferenceOutput/VisualTest_NotoEmoji_WeightVariations_-Bold--700.png index 24086b094..3c0d20bb1 100644 --- a/tests/Images/ReferenceOutput/VisualTest_NotoEmoji_WeightVariations_-Bold--700.png +++ b/tests/Images/ReferenceOutput/VisualTest_NotoEmoji_WeightVariations_-Bold--700.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dc19e53af674519c7da8a74f58873c3e567f5b6052431d8b3c5297a4296cdb9c -size 3274 +oid sha256:438762a421a5823664d76636ac73e7c25503f5004f266db7277cb8c32691b4d2 +size 3198 diff --git a/tests/Images/ReferenceOutput/VisualTest_NotoEmoji_WeightVariations_-Light--300.png b/tests/Images/ReferenceOutput/VisualTest_NotoEmoji_WeightVariations_-Light--300.png index 9b664e6cb..b063029e4 100644 --- a/tests/Images/ReferenceOutput/VisualTest_NotoEmoji_WeightVariations_-Light--300.png +++ b/tests/Images/ReferenceOutput/VisualTest_NotoEmoji_WeightVariations_-Light--300.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ad50c3a2d009485350a92186147350c51cbda7e38fd45aa778487d4ca934bcd4 -size 2744 +oid sha256:31be27b7a7ed3415f5f8a466e02b1e324643efcfc7387d0405ec3a9c830d55ca +size 2700 diff --git a/tests/Images/ReferenceOutput/VisualTest_NotoEmoji_WeightVariations_-Regular--400.png b/tests/Images/ReferenceOutput/VisualTest_NotoEmoji_WeightVariations_-Regular--400.png index 0685f42f3..d60283143 100644 --- a/tests/Images/ReferenceOutput/VisualTest_NotoEmoji_WeightVariations_-Regular--400.png +++ b/tests/Images/ReferenceOutput/VisualTest_NotoEmoji_WeightVariations_-Regular--400.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6ca6aacaee181e8103ce36664469b45ec7b3c9de075c914e5d47d03bdf0655cd -size 3168 +oid sha256:e93f66706b947624067f3fa0f7539ef6f2a5472bdf2b440bd46f3b81b219a9af +size 3112 diff --git a/tests/Images/ReferenceOutput/VisualTest_PaintedEmojiWeight_-Black--900.png b/tests/Images/ReferenceOutput/VisualTest_PaintedEmojiWeight_-Black--900.png new file mode 100644 index 000000000..d9a1340a6 --- /dev/null +++ b/tests/Images/ReferenceOutput/VisualTest_PaintedEmojiWeight_-Black--900.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:abab9244e5bc48a4e2a3bac4e6149f5c56904c2c11646b7fdd2ab4e8dad259af +size 3889 diff --git a/tests/Images/ReferenceOutput/VisualTest_PaintedEmojiWeight_-Normal--400.png b/tests/Images/ReferenceOutput/VisualTest_PaintedEmojiWeight_-Normal--400.png new file mode 100644 index 000000000..d9a1340a6 --- /dev/null +++ b/tests/Images/ReferenceOutput/VisualTest_PaintedEmojiWeight_-Normal--400.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:abab9244e5bc48a4e2a3bac4e6149f5c56904c2c11646b7fdd2ab4e8dad259af +size 3889 diff --git a/tests/Images/ReferenceOutput/VisualTest_RobotoFlex_MultipleAxes-.png b/tests/Images/ReferenceOutput/VisualTest_RobotoFlex_MultipleAxes-.png index bfc62d247..fd7dbbc2a 100644 --- a/tests/Images/ReferenceOutput/VisualTest_RobotoFlex_MultipleAxes-.png +++ b/tests/Images/ReferenceOutput/VisualTest_RobotoFlex_MultipleAxes-.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6a99237774a3155b5b708d96caec7d2abf5fe319c35cfb76ef949aa93afe7416 -size 2869 +oid sha256:6465ade0f2488a8ad79f261c243108ae5bf52cd6b366616215d1adb8fb298631 +size 2826 diff --git a/tests/Images/ReferenceOutput/VisualTest_RobotoFlex_WeightVariations_-Bold--700.png b/tests/Images/ReferenceOutput/VisualTest_RobotoFlex_WeightVariations_-Bold--700.png index b768d2d77..5fe2189b0 100644 --- a/tests/Images/ReferenceOutput/VisualTest_RobotoFlex_WeightVariations_-Bold--700.png +++ b/tests/Images/ReferenceOutput/VisualTest_RobotoFlex_WeightVariations_-Bold--700.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:80f2294b80b7013df31a92853fbc2bd3c851bad32ac2db57cb203523709737f6 -size 5140 +oid sha256:57f9a91ba83b3582499ce7d74667eb4b6f6f06fe91e80e4adf61a469e32795ae +size 5076 diff --git a/tests/Images/ReferenceOutput/VisualTest_RobotoFlex_WeightVariations_-Heavy--1000.png b/tests/Images/ReferenceOutput/VisualTest_RobotoFlex_WeightVariations_-Heavy--1000.png index fba3ffccf..df234b2b2 100644 --- a/tests/Images/ReferenceOutput/VisualTest_RobotoFlex_WeightVariations_-Heavy--1000.png +++ b/tests/Images/ReferenceOutput/VisualTest_RobotoFlex_WeightVariations_-Heavy--1000.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8e93b54e98907a3ce84feb65d576bd25c98115e38b7b88fd1242c2e73efe85ca -size 4971 +oid sha256:8f766808ac98fa29af8b2870438f3523a3ade5e330a573731ec70cc172c7a28e +size 4927 diff --git a/tests/Images/ReferenceOutput/VisualTest_RobotoFlex_WeightVariations_-Regular--400.png b/tests/Images/ReferenceOutput/VisualTest_RobotoFlex_WeightVariations_-Regular--400.png index 3cccdba84..bd92c6fbe 100644 --- a/tests/Images/ReferenceOutput/VisualTest_RobotoFlex_WeightVariations_-Regular--400.png +++ b/tests/Images/ReferenceOutput/VisualTest_RobotoFlex_WeightVariations_-Regular--400.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f7303182e3ddad8327586f07e08ff60c9743e36d22c93411eb290617988a7cf9 -size 5236 +oid sha256:cadecff3ecb63d6e9cb7af7c947b9d9a471fe743836a759ed5da56235eb632a1 +size 5258 diff --git a/tests/Images/ReferenceOutput/VisualTest_RobotoFlex_WeightVariations_-Thin--100.png b/tests/Images/ReferenceOutput/VisualTest_RobotoFlex_WeightVariations_-Thin--100.png index 22c4dc128..e5c10e328 100644 --- a/tests/Images/ReferenceOutput/VisualTest_RobotoFlex_WeightVariations_-Thin--100.png +++ b/tests/Images/ReferenceOutput/VisualTest_RobotoFlex_WeightVariations_-Thin--100.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2760ba2a11b40fe38532d94b36e31e16c10469f49f9e5c973e63ec2487ec5290 -size 4916 +oid sha256:6eecd4626adf2c162fd7932136586bd319750888843905774a6e8e474b765659 +size 5090 diff --git a/tests/Images/ReferenceOutput/VisualTest_StaticWeight_-Black--900.png b/tests/Images/ReferenceOutput/VisualTest_StaticWeight_-Black--900.png new file mode 100644 index 000000000..73cba079f --- /dev/null +++ b/tests/Images/ReferenceOutput/VisualTest_StaticWeight_-Black--900.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:07bf19d73a1799bff7e7e071d0355c847f0964a4d2cbbbbc6aad363289a453f0 +size 3693 diff --git a/tests/Images/ReferenceOutput/VisualTest_StaticWeight_-Bold--700.png b/tests/Images/ReferenceOutput/VisualTest_StaticWeight_-Bold--700.png new file mode 100644 index 000000000..73cba079f --- /dev/null +++ b/tests/Images/ReferenceOutput/VisualTest_StaticWeight_-Bold--700.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:07bf19d73a1799bff7e7e071d0355c847f0964a4d2cbbbbc6aad363289a453f0 +size 3693 diff --git a/tests/Images/ReferenceOutput/VisualTest_StaticWeight_-ExtraBold--800.png b/tests/Images/ReferenceOutput/VisualTest_StaticWeight_-ExtraBold--800.png new file mode 100644 index 000000000..73cba079f --- /dev/null +++ b/tests/Images/ReferenceOutput/VisualTest_StaticWeight_-ExtraBold--800.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:07bf19d73a1799bff7e7e071d0355c847f0964a4d2cbbbbc6aad363289a453f0 +size 3693 diff --git a/tests/Images/ReferenceOutput/VisualTest_StaticWeight_-ExtraLight--200.png b/tests/Images/ReferenceOutput/VisualTest_StaticWeight_-ExtraLight--200.png new file mode 100644 index 000000000..613d60209 --- /dev/null +++ b/tests/Images/ReferenceOutput/VisualTest_StaticWeight_-ExtraLight--200.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b6fbbdf7f8a462179ec40a4c3962c0b13dba7fd22e8a3405dc42346b248a6505 +size 3534 diff --git a/tests/Images/ReferenceOutput/VisualTest_StaticWeight_-Light--300.png b/tests/Images/ReferenceOutput/VisualTest_StaticWeight_-Light--300.png new file mode 100644 index 000000000..613d60209 --- /dev/null +++ b/tests/Images/ReferenceOutput/VisualTest_StaticWeight_-Light--300.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b6fbbdf7f8a462179ec40a4c3962c0b13dba7fd22e8a3405dc42346b248a6505 +size 3534 diff --git a/tests/Images/ReferenceOutput/VisualTest_StaticWeight_-Medium--500.png b/tests/Images/ReferenceOutput/VisualTest_StaticWeight_-Medium--500.png new file mode 100644 index 000000000..613d60209 --- /dev/null +++ b/tests/Images/ReferenceOutput/VisualTest_StaticWeight_-Medium--500.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b6fbbdf7f8a462179ec40a4c3962c0b13dba7fd22e8a3405dc42346b248a6505 +size 3534 diff --git a/tests/Images/ReferenceOutput/VisualTest_StaticWeight_-Normal--400.png b/tests/Images/ReferenceOutput/VisualTest_StaticWeight_-Normal--400.png new file mode 100644 index 000000000..613d60209 --- /dev/null +++ b/tests/Images/ReferenceOutput/VisualTest_StaticWeight_-Normal--400.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b6fbbdf7f8a462179ec40a4c3962c0b13dba7fd22e8a3405dc42346b248a6505 +size 3534 diff --git a/tests/Images/ReferenceOutput/VisualTest_StaticWeight_-SemiBold--600.png b/tests/Images/ReferenceOutput/VisualTest_StaticWeight_-SemiBold--600.png new file mode 100644 index 000000000..73cba079f --- /dev/null +++ b/tests/Images/ReferenceOutput/VisualTest_StaticWeight_-SemiBold--600.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:07bf19d73a1799bff7e7e071d0355c847f0964a4d2cbbbbc6aad363289a453f0 +size 3693 diff --git a/tests/Images/ReferenceOutput/VisualTest_StaticWeight_-Thin--100.png b/tests/Images/ReferenceOutput/VisualTest_StaticWeight_-Thin--100.png new file mode 100644 index 000000000..613d60209 --- /dev/null +++ b/tests/Images/ReferenceOutput/VisualTest_StaticWeight_-Thin--100.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b6fbbdf7f8a462179ec40a4c3962c0b13dba7fd22e8a3405dc42346b248a6505 +size 3534 diff --git a/tests/Images/ReferenceOutput/VisualTest_SyntheticBoldPaintedEmoji_-COLRv0-.png b/tests/Images/ReferenceOutput/VisualTest_SyntheticBoldPaintedEmoji_-COLRv0-.png new file mode 100644 index 000000000..d0ff958c0 --- /dev/null +++ b/tests/Images/ReferenceOutput/VisualTest_SyntheticBoldPaintedEmoji_-COLRv0-.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dad2bbf8ec166dc27798937d23bbbab21b368c380256fd3abf4fd1ba808ae4e9 +size 8109 diff --git a/tests/Images/ReferenceOutput/VisualTest_SyntheticBold_-CFF-.png b/tests/Images/ReferenceOutput/VisualTest_SyntheticBold_-CFF-.png new file mode 100644 index 000000000..98cc7a509 --- /dev/null +++ b/tests/Images/ReferenceOutput/VisualTest_SyntheticBold_-CFF-.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:875bab0ff0821ae20391b160f5bf7f7f111c8dcecd3be7cf97c7738fa36b93b7 +size 14088 diff --git a/tests/Images/ReferenceOutput/VisualTest_SyntheticBold_-TrueType-.png b/tests/Images/ReferenceOutput/VisualTest_SyntheticBold_-TrueType-.png new file mode 100644 index 000000000..f32f8a5c5 --- /dev/null +++ b/tests/Images/ReferenceOutput/VisualTest_SyntheticBold_-TrueType-.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cb4db8ab5523a8bfc8791ca57a382bd5b8b5e9d1d75b0c5423d439391cc6ba8f +size 13966 diff --git a/tests/Images/ReferenceOutput/VisualTest_SyntheticItalicPaintedEmoji_-COLRv0-.png b/tests/Images/ReferenceOutput/VisualTest_SyntheticItalicPaintedEmoji_-COLRv0-.png new file mode 100644 index 000000000..2dd109295 --- /dev/null +++ b/tests/Images/ReferenceOutput/VisualTest_SyntheticItalicPaintedEmoji_-COLRv0-.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9de37c079a11f63750405cf00b09d0ccb75f64dd95f1ee703ebfe01305e502ec +size 8420 diff --git a/tests/Images/ReferenceOutput/VisualTest_SyntheticItalic_-CFF-.png b/tests/Images/ReferenceOutput/VisualTest_SyntheticItalic_-CFF-.png new file mode 100644 index 000000000..059afec52 --- /dev/null +++ b/tests/Images/ReferenceOutput/VisualTest_SyntheticItalic_-CFF-.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e908e4e787126e70f658b4751dec32e0b6afca50ba3c37bafc5bb958d399a207 +size 14918 diff --git a/tests/Images/ReferenceOutput/VisualTest_SyntheticItalic_-TrueType-.png b/tests/Images/ReferenceOutput/VisualTest_SyntheticItalic_-TrueType-.png new file mode 100644 index 000000000..61d6de05e --- /dev/null +++ b/tests/Images/ReferenceOutput/VisualTest_SyntheticItalic_-TrueType-.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:59da097e9edbb4bf29bf639e5f8d93be3b16f21205798099d9fc89b5a94f1e5a +size 15107 diff --git a/tests/Images/ReferenceOutput/VisualTest_SystemArialWeight_-Black--900.png b/tests/Images/ReferenceOutput/VisualTest_SystemArialWeight_-Black--900.png new file mode 100644 index 000000000..f9adb4b37 --- /dev/null +++ b/tests/Images/ReferenceOutput/VisualTest_SystemArialWeight_-Black--900.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2ac1b27090be3748564c505dab36e24ca97d4e65baf8dd4c8e52dc581fe25f5b +size 3642 diff --git a/tests/Images/ReferenceOutput/VisualTest_SystemArialWeight_-Bold--700.png b/tests/Images/ReferenceOutput/VisualTest_SystemArialWeight_-Bold--700.png new file mode 100644 index 000000000..42554b258 --- /dev/null +++ b/tests/Images/ReferenceOutput/VisualTest_SystemArialWeight_-Bold--700.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9192baf859b8cf1e35c3e8a92f52a835aa5bcb31cc15bba61577163d4515e063 +size 3554 diff --git a/tests/Images/ReferenceOutput/VisualTest_SystemArialWeight_-ExtraBold--800.png b/tests/Images/ReferenceOutput/VisualTest_SystemArialWeight_-ExtraBold--800.png new file mode 100644 index 000000000..f9adb4b37 --- /dev/null +++ b/tests/Images/ReferenceOutput/VisualTest_SystemArialWeight_-ExtraBold--800.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2ac1b27090be3748564c505dab36e24ca97d4e65baf8dd4c8e52dc581fe25f5b +size 3642 diff --git a/tests/Images/ReferenceOutput/VisualTest_SystemArialWeight_-ExtraLight--200.png b/tests/Images/ReferenceOutput/VisualTest_SystemArialWeight_-ExtraLight--200.png new file mode 100644 index 000000000..f754a32cb --- /dev/null +++ b/tests/Images/ReferenceOutput/VisualTest_SystemArialWeight_-ExtraLight--200.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0e02343d32ead866db6c139531a97149a86174d7d23a77935fcb4f479f78ecad +size 3395 diff --git a/tests/Images/ReferenceOutput/VisualTest_SystemArialWeight_-Light--300.png b/tests/Images/ReferenceOutput/VisualTest_SystemArialWeight_-Light--300.png new file mode 100644 index 000000000..f754a32cb --- /dev/null +++ b/tests/Images/ReferenceOutput/VisualTest_SystemArialWeight_-Light--300.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0e02343d32ead866db6c139531a97149a86174d7d23a77935fcb4f479f78ecad +size 3395 diff --git a/tests/Images/ReferenceOutput/VisualTest_SystemArialWeight_-Medium--500.png b/tests/Images/ReferenceOutput/VisualTest_SystemArialWeight_-Medium--500.png new file mode 100644 index 000000000..f754a32cb --- /dev/null +++ b/tests/Images/ReferenceOutput/VisualTest_SystemArialWeight_-Medium--500.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0e02343d32ead866db6c139531a97149a86174d7d23a77935fcb4f479f78ecad +size 3395 diff --git a/tests/Images/ReferenceOutput/VisualTest_SystemArialWeight_-Normal--400.png b/tests/Images/ReferenceOutput/VisualTest_SystemArialWeight_-Normal--400.png new file mode 100644 index 000000000..f754a32cb --- /dev/null +++ b/tests/Images/ReferenceOutput/VisualTest_SystemArialWeight_-Normal--400.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0e02343d32ead866db6c139531a97149a86174d7d23a77935fcb4f479f78ecad +size 3395 diff --git a/tests/Images/ReferenceOutput/VisualTest_SystemArialWeight_-SemiBold--600.png b/tests/Images/ReferenceOutput/VisualTest_SystemArialWeight_-SemiBold--600.png new file mode 100644 index 000000000..42554b258 --- /dev/null +++ b/tests/Images/ReferenceOutput/VisualTest_SystemArialWeight_-SemiBold--600.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9192baf859b8cf1e35c3e8a92f52a835aa5bcb31cc15bba61577163d4515e063 +size 3554 diff --git a/tests/Images/ReferenceOutput/VisualTest_SystemArialWeight_-Thin--100.png b/tests/Images/ReferenceOutput/VisualTest_SystemArialWeight_-Thin--100.png new file mode 100644 index 000000000..f754a32cb --- /dev/null +++ b/tests/Images/ReferenceOutput/VisualTest_SystemArialWeight_-Thin--100.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0e02343d32ead866db6c139531a97149a86174d7d23a77935fcb4f479f78ecad +size 3395 diff --git a/tests/Images/ReferenceOutput/VisualTest_SystemSegoeUIWeight_-Black--900.png b/tests/Images/ReferenceOutput/VisualTest_SystemSegoeUIWeight_-Black--900.png new file mode 100644 index 000000000..b2c1fa1c7 --- /dev/null +++ b/tests/Images/ReferenceOutput/VisualTest_SystemSegoeUIWeight_-Black--900.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2d4bd14e97a1020f9173c1b6200592ecaeda8fa35d04a3eaad66bb2d30fc0a8f +size 3326 diff --git a/tests/Images/ReferenceOutput/VisualTest_SystemSegoeUIWeight_-Bold--700.png b/tests/Images/ReferenceOutput/VisualTest_SystemSegoeUIWeight_-Bold--700.png new file mode 100644 index 000000000..841f14d1b --- /dev/null +++ b/tests/Images/ReferenceOutput/VisualTest_SystemSegoeUIWeight_-Bold--700.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5eb6432469c44a1dd23e55d249e6a9489db7264fde484dcfb522e9d3c5adf39e +size 3468 diff --git a/tests/Images/ReferenceOutput/VisualTest_SystemSegoeUIWeight_-ExtraBold--800.png b/tests/Images/ReferenceOutput/VisualTest_SystemSegoeUIWeight_-ExtraBold--800.png new file mode 100644 index 000000000..b2c1fa1c7 --- /dev/null +++ b/tests/Images/ReferenceOutput/VisualTest_SystemSegoeUIWeight_-ExtraBold--800.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2d4bd14e97a1020f9173c1b6200592ecaeda8fa35d04a3eaad66bb2d30fc0a8f +size 3326 diff --git a/tests/Images/ReferenceOutput/VisualTest_SystemSegoeUIWeight_-ExtraLight--200.png b/tests/Images/ReferenceOutput/VisualTest_SystemSegoeUIWeight_-ExtraLight--200.png new file mode 100644 index 000000000..826d8db94 --- /dev/null +++ b/tests/Images/ReferenceOutput/VisualTest_SystemSegoeUIWeight_-ExtraLight--200.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:821fd2f82cec27556c761612efde22612ddc40e8af77dbc7e3c4251383f5a636 +size 3257 diff --git a/tests/Images/ReferenceOutput/VisualTest_SystemSegoeUIWeight_-Light--300.png b/tests/Images/ReferenceOutput/VisualTest_SystemSegoeUIWeight_-Light--300.png new file mode 100644 index 000000000..826d8db94 --- /dev/null +++ b/tests/Images/ReferenceOutput/VisualTest_SystemSegoeUIWeight_-Light--300.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:821fd2f82cec27556c761612efde22612ddc40e8af77dbc7e3c4251383f5a636 +size 3257 diff --git a/tests/Images/ReferenceOutput/VisualTest_SystemSegoeUIWeight_-Medium--500.png b/tests/Images/ReferenceOutput/VisualTest_SystemSegoeUIWeight_-Medium--500.png new file mode 100644 index 000000000..31509d03e --- /dev/null +++ b/tests/Images/ReferenceOutput/VisualTest_SystemSegoeUIWeight_-Medium--500.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fd5f756d5cf7360a79b386b5bb934b0146a030beee66c18c446e613f004ec112 +size 3419 diff --git a/tests/Images/ReferenceOutput/VisualTest_SystemSegoeUIWeight_-Normal--400.png b/tests/Images/ReferenceOutput/VisualTest_SystemSegoeUIWeight_-Normal--400.png new file mode 100644 index 000000000..f827ea18f --- /dev/null +++ b/tests/Images/ReferenceOutput/VisualTest_SystemSegoeUIWeight_-Normal--400.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:225c42ea786ad38031f19dbd5aae838abf8f323590497c52c337d273ca0a4435 +size 3341 diff --git a/tests/Images/ReferenceOutput/VisualTest_SystemSegoeUIWeight_-SemiBold--600.png b/tests/Images/ReferenceOutput/VisualTest_SystemSegoeUIWeight_-SemiBold--600.png new file mode 100644 index 000000000..31509d03e --- /dev/null +++ b/tests/Images/ReferenceOutput/VisualTest_SystemSegoeUIWeight_-SemiBold--600.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fd5f756d5cf7360a79b386b5bb934b0146a030beee66c18c446e613f004ec112 +size 3419 diff --git a/tests/Images/ReferenceOutput/VisualTest_SystemSegoeUIWeight_-Thin--100.png b/tests/Images/ReferenceOutput/VisualTest_SystemSegoeUIWeight_-Thin--100.png new file mode 100644 index 000000000..826d8db94 --- /dev/null +++ b/tests/Images/ReferenceOutput/VisualTest_SystemSegoeUIWeight_-Thin--100.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:821fd2f82cec27556c761612efde22612ddc40e8af77dbc7e3c4251383f5a636 +size 3257 diff --git a/tests/Images/ReferenceOutput/VisualTest_VariableWeight_-Black--900.png b/tests/Images/ReferenceOutput/VisualTest_VariableWeight_-Black--900.png new file mode 100644 index 000000000..45fbd8b30 --- /dev/null +++ b/tests/Images/ReferenceOutput/VisualTest_VariableWeight_-Black--900.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e170deb3db3d2b423ea761968109438c3e1bd893cbd5da1924e9ba11cee2882f +size 3399 diff --git a/tests/Images/ReferenceOutput/VisualTest_VariableWeight_-Bold--700.png b/tests/Images/ReferenceOutput/VisualTest_VariableWeight_-Bold--700.png new file mode 100644 index 000000000..61196614c --- /dev/null +++ b/tests/Images/ReferenceOutput/VisualTest_VariableWeight_-Bold--700.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:22bdc47879a19e89e9f553f512feb0fd6d8069c869c7c019935c31e8d318908c +size 3398 diff --git a/tests/Images/ReferenceOutput/VisualTest_VariableWeight_-ExtraBold--800.png b/tests/Images/ReferenceOutput/VisualTest_VariableWeight_-ExtraBold--800.png new file mode 100644 index 000000000..df957b9ae --- /dev/null +++ b/tests/Images/ReferenceOutput/VisualTest_VariableWeight_-ExtraBold--800.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aa05edd1ced66dcaa3c45006884c239fc2d00d800605004a841e75c62eadf308 +size 3462 diff --git a/tests/Images/ReferenceOutput/VisualTest_VariableWeight_-ExtraLight--200.png b/tests/Images/ReferenceOutput/VisualTest_VariableWeight_-ExtraLight--200.png new file mode 100644 index 000000000..dad212edc --- /dev/null +++ b/tests/Images/ReferenceOutput/VisualTest_VariableWeight_-ExtraLight--200.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:989d750c1912a2e45715ca1dccdd8970e5407d3c31bab04388adbae19645e237 +size 3272 diff --git a/tests/Images/ReferenceOutput/VisualTest_VariableWeight_-Light--300.png b/tests/Images/ReferenceOutput/VisualTest_VariableWeight_-Light--300.png new file mode 100644 index 000000000..4701c6dc5 --- /dev/null +++ b/tests/Images/ReferenceOutput/VisualTest_VariableWeight_-Light--300.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:020f229b0f590cb8cc3f02cb20eec18b1e56f0a19764bac7056d0ce9efe47fbe +size 3325 diff --git a/tests/Images/ReferenceOutput/VisualTest_VariableWeight_-Medium--500.png b/tests/Images/ReferenceOutput/VisualTest_VariableWeight_-Medium--500.png new file mode 100644 index 000000000..80385d969 --- /dev/null +++ b/tests/Images/ReferenceOutput/VisualTest_VariableWeight_-Medium--500.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:05ee8342b445cadc471d7b25d6388002837b88a18f4a7a359e75491eca4573a5 +size 3399 diff --git a/tests/Images/ReferenceOutput/VisualTest_VariableWeight_-Normal--400.png b/tests/Images/ReferenceOutput/VisualTest_VariableWeight_-Normal--400.png new file mode 100644 index 000000000..c1d7f4edb --- /dev/null +++ b/tests/Images/ReferenceOutput/VisualTest_VariableWeight_-Normal--400.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6539b4a1e94187cc6f07f116ec8f54b31057d93a231a8a8f4a750d4c7f8a71e4 +size 3348 diff --git a/tests/Images/ReferenceOutput/VisualTest_VariableWeight_-SemiBold--600.png b/tests/Images/ReferenceOutput/VisualTest_VariableWeight_-SemiBold--600.png new file mode 100644 index 000000000..6fc1db769 --- /dev/null +++ b/tests/Images/ReferenceOutput/VisualTest_VariableWeight_-SemiBold--600.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6a9941e5996dd47252eb9c90b4d8075fbde0a34b718ac6fc6d5d57c9ab7f6864 +size 3414 diff --git a/tests/Images/ReferenceOutput/VisualTest_VariableWeight_-Thin--100.png b/tests/Images/ReferenceOutput/VisualTest_VariableWeight_-Thin--100.png new file mode 100644 index 000000000..0060d58d5 --- /dev/null +++ b/tests/Images/ReferenceOutput/VisualTest_VariableWeight_-Thin--100.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ef642188ae7788a69ad5c276676b5daf74a07b3b4bc270c2653551a6be75492e +size 3159 diff --git a/tests/Images/ReferenceOutput/VisualTest_VotoSerif_CVar_WeightVariations_-Heavy--194-None.png b/tests/Images/ReferenceOutput/VisualTest_VotoSerif_CVar_WeightVariations_-Heavy--194-None.png index a4e6c5b04..847f7835f 100644 --- a/tests/Images/ReferenceOutput/VisualTest_VotoSerif_CVar_WeightVariations_-Heavy--194-None.png +++ b/tests/Images/ReferenceOutput/VisualTest_VotoSerif_CVar_WeightVariations_-Heavy--194-None.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0576e16d9b0bf7c618d7c528b10fa60eb95e6fe8150b9a18770ac67f238bbf77 -size 1142 +oid sha256:a61d24d0cf7b2cc2fcbd3f20bc775f6f49221f253221a422399345929da9592d +size 1130 diff --git a/tests/Images/ReferenceOutput/VisualTest_VotoSerif_CVar_WeightVariations_-Heavy--194-Standard.png b/tests/Images/ReferenceOutput/VisualTest_VotoSerif_CVar_WeightVariations_-Heavy--194-Standard.png index 8431f11e9..33a53d4c9 100644 --- a/tests/Images/ReferenceOutput/VisualTest_VotoSerif_CVar_WeightVariations_-Heavy--194-Standard.png +++ b/tests/Images/ReferenceOutput/VisualTest_VotoSerif_CVar_WeightVariations_-Heavy--194-Standard.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c0aa4e751a3fca65ee835c045eeab8bdf966c26177d8e5f6fcb5e1066d3a5943 -size 1125 +oid sha256:abbdc6ffd64b8e786f57cb909c585086e6d407757d8378439747f195fc42ce59 +size 1120 diff --git a/tests/Images/ReferenceOutput/VisualTest_VotoSerif_CVar_WeightVariations_-Light--28-None.png b/tests/Images/ReferenceOutput/VisualTest_VotoSerif_CVar_WeightVariations_-Light--28-None.png index 613322f37..4aa5fc8a6 100644 --- a/tests/Images/ReferenceOutput/VisualTest_VotoSerif_CVar_WeightVariations_-Light--28-None.png +++ b/tests/Images/ReferenceOutput/VisualTest_VotoSerif_CVar_WeightVariations_-Light--28-None.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b8eb2042ca9d272c3beb341feb588478d5b765ce749acc666a64e4761346c78b -size 1025 +oid sha256:31f1ee14bb3c4f5d31b4d97c7657fb89e0d55eff9c82b77402426163b7edfb21 +size 1055 diff --git a/tests/Images/ReferenceOutput/VisualTest_VotoSerif_CVar_WeightVariations_-Light--28-Standard.png b/tests/Images/ReferenceOutput/VisualTest_VotoSerif_CVar_WeightVariations_-Light--28-Standard.png index b36b6d4ef..d86642b1b 100644 --- a/tests/Images/ReferenceOutput/VisualTest_VotoSerif_CVar_WeightVariations_-Light--28-Standard.png +++ b/tests/Images/ReferenceOutput/VisualTest_VotoSerif_CVar_WeightVariations_-Light--28-Standard.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b760621a6f2abd6b9253e64d611a50fa43916d24a83a4cd75a6d8f45fa4eee88 -size 1038 +oid sha256:9948b378b401ea48fa0a03e197b81bfafed7c917b44da18a6de66c016dd6f3b9 +size 1041 diff --git a/tests/Images/ReferenceOutput/VisualTest_VotoSerif_CVar_WeightVariations_-Regular--94-None.png b/tests/Images/ReferenceOutput/VisualTest_VotoSerif_CVar_WeightVariations_-Regular--94-None.png index cab3f844f..39d2a2295 100644 --- a/tests/Images/ReferenceOutput/VisualTest_VotoSerif_CVar_WeightVariations_-Regular--94-None.png +++ b/tests/Images/ReferenceOutput/VisualTest_VotoSerif_CVar_WeightVariations_-Regular--94-None.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ba1625ad8d5118830fd22a574396db6094b143e50573f34254247b1d8f53a911 -size 1191 +oid sha256:4a9b84f22335250cde85a96d38894996e1ac9f101417d88bfc805186ee3f37e0 +size 1226 diff --git a/tests/Images/ReferenceOutput/VisualTest_VotoSerif_CVar_WeightVariations_-Regular--94-Standard.png b/tests/Images/ReferenceOutput/VisualTest_VotoSerif_CVar_WeightVariations_-Regular--94-Standard.png index 2f46dc26d..7195ba7f6 100644 --- a/tests/Images/ReferenceOutput/VisualTest_VotoSerif_CVar_WeightVariations_-Regular--94-Standard.png +++ b/tests/Images/ReferenceOutput/VisualTest_VotoSerif_CVar_WeightVariations_-Regular--94-Standard.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:95792604bec6d9a77780fd875e8d747c2a4b30a039bec3cf33b4cdbb78cc78ea -size 1205 +oid sha256:ef5cd5847bf8c6ddce094d446d6c3b1e5d304ccfce46dfe8d0294e58cbcf4a20 +size 1162 diff --git a/tests/Images/ReferenceOutput/WordMetrics_GetSelectionBounds_DrawsWordSelections-.png b/tests/Images/ReferenceOutput/WordMetrics_GetSelectionBounds_DrawsWordSelections-.png index 1b0c71a06..a89263afa 100644 --- a/tests/Images/ReferenceOutput/WordMetrics_GetSelectionBounds_DrawsWordSelections-.png +++ b/tests/Images/ReferenceOutput/WordMetrics_GetSelectionBounds_DrawsWordSelections-.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:df4eb8843740d05ab42c89809ca4b68235ec3673a08c331a0a9cee118c824074 -size 4860 +oid sha256:c9db8fd2b3277b75429aa908d61d92d17771ea4d1128354d6c307b46c6895b21 +size 4960 diff --git a/tests/SixLabors.Fonts.Tests/AppContextSwitchAttribute.cs b/tests/SixLabors.Fonts.Tests/AppContextSwitchAttribute.cs deleted file mode 100644 index 005ff69c8..000000000 --- a/tests/SixLabors.Fonts.Tests/AppContextSwitchAttribute.cs +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Reflection; -using Xunit.Sdk; - -namespace SixLabors.Fonts.Tests; - -/// -/// Sets the AppContext switch to before the test execution -/// and resets the switch after the text execution. -/// -public class AppContextSwitchAttribute : BeforeAfterTestAttribute -{ - public string SwitchName { get; } - - public bool IsEnabled { get; } - - public AppContextSwitchAttribute(string switchName, bool isEnabled) - { - this.SwitchName = switchName; - this.IsEnabled = isEnabled; - } - - public override void Before(MethodInfo methodUnderTest) - => AppDomain.CurrentDomain.SetData(this.SwitchName, this.IsEnabled.ToString()); - - public override void After(MethodInfo methodUnderTest) - => AppDomain.CurrentDomain.SetData(this.SwitchName, null); -} diff --git a/tests/SixLabors.Fonts.Tests/CompactFontFormatTests.cs b/tests/SixLabors.Fonts.Tests/CompactFontFormatTests.cs index ca98a0aa1..c866104d7 100644 --- a/tests/SixLabors.Fonts.Tests/CompactFontFormatTests.cs +++ b/tests/SixLabors.Fonts.Tests/CompactFontFormatTests.cs @@ -19,7 +19,7 @@ public void FDSelectFormat0_Works(string testStr, int expectedGlyphIndex) ColorGlyphRenderer renderer = new(); // act - TextRenderer.RenderTextTo(renderer, testStr, new TextOptions(font)); + TextRenderer.RenderTo(renderer, testStr, new TextOptions(font)); // assert GlyphRendererParameters glyphKey = Assert.Single(renderer.GlyphKeys); diff --git a/tests/SixLabors.Fonts.Tests/Fakes/FakeFontInstance.cs b/tests/SixLabors.Fonts.Tests/Fakes/FakeFontInstance.cs index f7b3756dc..8716babdf 100644 --- a/tests/SixLabors.Fonts.Tests/Fakes/FakeFontInstance.cs +++ b/tests/SixLabors.Fonts.Tests/Fakes/FakeFontInstance.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.Fonts.Tables.AdvancedTypographic; using SixLabors.Fonts.Tables.General; using SixLabors.Fonts.Tables.General.Kern; using SixLabors.Fonts.Tables.General.Name; @@ -12,13 +13,15 @@ namespace SixLabors.Fonts.Tests.Fakes; internal class FakeFontInstance : StreamFontMetrics { + private static readonly FontSource SyntheticSource = new SyntheticFontSource(); + internal FakeFontInstance(string text) : this(CreateTrueTypeFontTables(text)) { } internal FakeFontInstance(TrueTypeFontTables tables) - : base(tables) + : base(tables, SyntheticSource) { } @@ -137,4 +140,17 @@ private static HeadTable GenerateHeadTable() HeadTable.IndexLocationFormats.Offset16); private static PostTable GeneratePostTable() => new(2, 0, 0, 200, 35, 0, 0, 0, 0, 0, Array.Empty()); + + private sealed class SyntheticFontSource : FontSource + { + /// + public override bool TryGetTableData(Tag tag, out ReadOnlyMemory table) + { + table = default; + return false; + } + + /// + public override Stream OpenStream() => new MemoryStream(Array.Empty(), writable: false); + } } diff --git a/tests/SixLabors.Fonts.Tests/FontCollectionTests.cs b/tests/SixLabors.Fonts.Tests/FontCollectionTests.cs index 1b4d9d0da..4f259aa68 100644 --- a/tests/SixLabors.Fonts.Tests/FontCollectionTests.cs +++ b/tests/SixLabors.Fonts.Tests/FontCollectionTests.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Globalization; +using SixLabors.Fonts.Unicode; namespace SixLabors.Fonts.Tests; @@ -45,6 +46,38 @@ public void AddViaStreamReturnsDescription() Assert.Equal(FontStyle.Regular, description.Style); } + [Fact] + public void AddViaStreamAddMemoryFontFileInstances() + { + FontCollection sut = new(); + using Stream s = TestFonts.CarterOneFileData(); + FontFamily family = sut.Add(s, out FontDescription description); + + IEnumerable allInstances = ((IReadOnlyFontMetricsCollection)sut).GetAllMetrics(family.Name, CultureInfo.InvariantCulture); + + Assert.All(allInstances, i => + { + MemoryFontMetrics font = Assert.IsType(i); + }); + } + + [Fact] + public void AddViaStreamCanUseFontAfterSourceDisposed() + { + FontCollection sut = new(); + FontFamily family; + + using (Stream stream = TestFonts.CarterOneFileData()) + { + family = sut.Add(stream); + } + + Font font = family.CreateFont(12); + + Assert.True(font.TryGetGlyphId(new CodePoint('A'), out ushort glyphId)); + Assert.NotEqual(0, glyphId); + } + [Fact] public void NotFoundThrowsCorrectException() { diff --git a/tests/SixLabors.Fonts.Tests/FontLoaderTests.cs b/tests/SixLabors.Fonts.Tests/FontLoaderTests.cs index 577b3cec4..ddd28b373 100644 --- a/tests/SixLabors.Fonts.Tests/FontLoaderTests.cs +++ b/tests/SixLabors.Fonts.Tests/FontLoaderTests.cs @@ -48,7 +48,7 @@ public void LoadFont_WithTtfFormat() Assert.True(font.TryGetGlyphs(new CodePoint('A'), ColorFontSupport.None, out Glyph? glyph)); GlyphRenderer r = new(); - glyph.Value.RenderTo(r, 0, Vector2.Zero, Vector2.Zero, GlyphLayoutMode.Horizontal, new TextOptions(font)); + glyph.Value.RenderTo(r, 0, Vector2.Zero, Vector2.Zero, new Vector2(-1F), GlyphLayoutMode.Horizontal, new TextOptions(font)); Assert.Equal(37, r.ControlPoints.Count); Assert.Single(r.GlyphKeys); @@ -62,7 +62,7 @@ public void LoadFont_WithWoff1Format() Assert.True(font.TryGetGlyphs(new CodePoint('A'), ColorFontSupport.None, out Glyph? glyph)); GlyphRenderer r = new(); - glyph.Value.RenderTo(r, 0, Vector2.Zero, Vector2.Zero, GlyphLayoutMode.Horizontal, new TextOptions(font)); + glyph.Value.RenderTo(r, 0, Vector2.Zero, Vector2.Zero, new Vector2(-1F), GlyphLayoutMode.Horizontal, new TextOptions(font)); Assert.Equal(37, r.ControlPoints.Count); Assert.Single(r.GlyphKeys); @@ -94,7 +94,7 @@ public void LoadFont_WithWoff2Format() Assert.True(font.TryGetGlyphs(new CodePoint('A'), ColorFontSupport.None, out Glyph? glyph)); GlyphRenderer r = new(); - glyph.Value.RenderTo(r, 0, Vector2.Zero, Vector2.Zero, GlyphLayoutMode.Horizontal, new TextOptions(font)); + glyph.Value.RenderTo(r, 0, Vector2.Zero, Vector2.Zero, new Vector2(-1F), GlyphLayoutMode.Horizontal, new TextOptions(font)); Assert.Equal(37, r.ControlPoints.Count); Assert.Single(r.GlyphKeys); @@ -111,7 +111,7 @@ public void LoadFont() Assert.True(font.TryGetGlyphs(new CodePoint('a'), ColorFontSupport.None, out Glyph? glyph)); GlyphRenderer r = new(); - glyph.Value.RenderTo(r, 0, Vector2.Zero, Vector2.Zero, GlyphLayoutMode.Horizontal, new TextOptions(font)); + glyph.Value.RenderTo(r, 0, Vector2.Zero, Vector2.Zero, new Vector2(-1F), GlyphLayoutMode.Horizontal, new TextOptions(font)); // the test font only has characters .notdef, 'a' & 'b' defined Assert.Equal(6, r.ControlPoints.Distinct().Count()); @@ -127,7 +127,7 @@ public void LoadFontWoff() Assert.True(font.TryGetGlyphs(new CodePoint('a'), ColorFontSupport.None, out Glyph? glyph)); GlyphRenderer r = new(); - glyph.Value.RenderTo(r, 0, Vector2.Zero, Vector2.Zero, GlyphLayoutMode.Horizontal, new TextOptions(font)); + glyph.Value.RenderTo(r, 0, Vector2.Zero, Vector2.Zero, new Vector2(-1F), 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/FontSynthesisTests.cs b/tests/SixLabors.Fonts.Tests/FontSynthesisTests.cs index c2678cec4..5902af967 100644 --- a/tests/SixLabors.Fonts.Tests/FontSynthesisTests.cs +++ b/tests/SixLabors.Fonts.Tests/FontSynthesisTests.cs @@ -9,8 +9,11 @@ namespace SixLabors.Fonts.Tests; public class FontSynthesisTests { - // tan(14 degrees) - the default oblique slant browsers apply for synthesized italics. - private static readonly float ExpectedSkew = MathF.Tan(14F * MathF.PI / 180F); + // Chromium and SkParagraph pass an exact quarter shear to SkFont for synthesized italics. + private const float ExpectedSkew = 1F / 4F; + + // A divisor of 31 matches Chromium's DirectWrite weight while retaining FreeType dilation. + private const float ExpectedBoldEmScale = 1F / 31F; private static readonly ApproximateFloatComparer SkewComparer = new(0.001F); @@ -19,6 +22,23 @@ public class FontSynthesisTests private static FontFamily RegularOnlyFamily(string file) => new FontCollection().Add(file); + private static TextOptions PaintedEmojiOptions(FontStyle style) + { + FontCollection collection = new(); + FontFamily emoji = collection.Add(TestFonts.TwemojiMozillaFile); + FontFamily fallback = collection.Add(TestFonts.OpenSansFile); + + // Twemoji does not contain U+0020, so use the same explicit text fallback as the browser + // comparison instead of measuring the full-em missing-glyph advance between emoji. + return new TextOptions(new Font(emoji, 54, style)) + { + Dpi = 96F, + LineSpacing = 1.4F, + ColorFontSupport = ColorFontSupport.ColrV0, + FallbackFontFamilies = [fallback] + }; + } + [Fact] public void FamilyWithoutItalic_FallsBackToRegularMetrics() { @@ -40,11 +60,76 @@ public void GetObliqueSkew_Cff_NonZeroOnlyWhenItalicSynthesized() [Fact] public void SyntheticItalic_ShearsGlyphOutline_TrueType() - => AssertSyntheticItalicShear(TestFonts.OpenSansFile, "Hg"); + => AssertSyntheticItalicShear(TestFonts.OpenSansFile, "Hg", ColorFontSupport.None); [Fact] public void SyntheticItalic_ShearsGlyphOutline_Cff() - => AssertSyntheticItalicShear(TestFonts.PlantinStdRegularFile, "Hg"); + => AssertSyntheticItalicShear(TestFonts.PlantinStdRegularFile, "Hg", ColorFontSupport.None); + + [Fact] + public void SyntheticItalic_ShearsPaintedEmoji() + => AssertSyntheticItalicShear(TestFonts.TwemojiMozillaFile, "😀", ColorFontSupport.ColrV0); + + [Fact] + public void SyntheticItalic_PaintedEmojiBoundsContainRenderedGeometry() + { + const string text = "😀 ☺️ ❤️ ✌️ ⭐"; + const float tolerance = .1F; + TextOptions options = PaintedEmojiOptions(FontStyle.Italic); + options.HintingMode = HintingMode.None; + + FontRectangle bounds = TextMeasurer.MeasureBounds(text, options); + GlyphRenderer renderer = new(); + TextRenderer.RenderTo(renderer, text, options); + + Assert.NotEmpty(renderer.ControlPoints); + foreach (Vector2 point in renderer.ControlPoints) + { + Assert.InRange(point.X, bounds.Left - tolerance, bounds.Right + tolerance); + Assert.InRange(point.Y, bounds.Top - tolerance, bounds.Bottom + tolerance); + } + } + + [Fact] + public void BoundsTransform_UsesAllFourCorners() + { + Bounds bounds = new(0F, 0F, 10F, 20F); + Bounds transformed = Bounds.Transform(bounds, Matrix3x2.CreateRotation(MathF.PI / 4F)); + + Assert.Equal(-14.142136F, transformed.Min.X, GeometryComparer); + Assert.Equal(0F, transformed.Min.Y, GeometryComparer); + Assert.Equal(7.071068F, transformed.Max.X, GeometryComparer); + Assert.Equal(21.213203F, transformed.Max.Y, GeometryComparer); + } + + [Fact] + public void VisualTest_SyntheticItalicPaintedEmoji() + { + TextOptions options = PaintedEmojiOptions(FontStyle.Italic); + + // Painted emoji retain their authored layers while the complete layered outline receives + // the same synthetic italic transform as monochrome glyphs. + TextLayoutTestUtilities.TestLayout("😀 ☺️ ❤️ ✌️ ⭐", options, properties: "COLRv0"); + } + + [Theory] + [InlineData("OpenSans-Regular.ttf", "TrueType")] + [InlineData("PlantinStdRegular.otf", "CFF")] + public void VisualTest_SyntheticItalic(string fontFile, string fontFormat) + { + const string text = "Sphinx of black quartz, judge my vow.\nPack my box with five dozen liquor jugs."; + string path = Path.Combine(TestEnvironment.FontTestDataFullPath, fontFile); + Font font = new(RegularOnlyFamily(path), 36, FontStyle.Italic); + TextOptions options = new(font) + { + Dpi = 96F, + LineSpacing = 1.4F + }; + + // The collection contains only the regular face, so requesting italic exercises the + // synthesized outline while using the standard rendering-test output and comparison path. + TextLayoutTestUtilities.TestLayout(text, options, properties: fontFormat); + } [Fact] public void SyntheticItalic_DoesNotChangeAdvance_ButWidensBounds() @@ -100,17 +185,25 @@ private static void AssertObliqueSkew(string file) Assert.Equal(ExpectedSkew, italicGlyph.Value.GlyphMetrics.GetObliqueSkew(), SkewComparer); } - private static void AssertSyntheticItalicShear(string file, string text) + private static void AssertSyntheticItalicShear(string file, string text, ColorFontSupport colorFontSupport) { FontFamily family = RegularOnlyFamily(file); Font regular = new(family, 48, FontStyle.Regular); Font italic = new(family, 48, FontStyle.Italic); GlyphRenderer regularRenderer = new(); - TextRenderer.RenderTextTo(regularRenderer, text, new TextOptions(regular) { HintingMode = HintingMode.None }); + TextRenderer.RenderTo(regularRenderer, text, new TextOptions(regular) + { + HintingMode = HintingMode.None, + ColorFontSupport = colorFontSupport + }); GlyphRenderer italicRenderer = new(); - TextRenderer.RenderTextTo(italicRenderer, text, new TextOptions(italic) { HintingMode = HintingMode.None }); + TextRenderer.RenderTo(italicRenderer, text, new TextOptions(italic) + { + HintingMode = HintingMode.None, + ColorFontSupport = colorFontSupport + }); List r = regularRenderer.ControlPoints; List i = italicRenderer.ControlPoints; @@ -154,6 +247,39 @@ public void ShouldSynthesizeBold_TrueType_TrueOnlyWhenBoldSynthesized() public void ShouldSynthesizeBold_Cff_TrueOnlyWhenBoldSynthesized() => AssertShouldSynthesizeBold(TestFonts.PlantinStdRegularFile); + [Fact] + public void SyntheticBoldStrength_MatchesBrowserWeight() + { + const float pointSize = 48F; + Font bold = new(RegularOnlyFamily(TestFonts.OpenSansFile), pointSize, FontStyle.Bold); + + Assert.True(bold.TryGetGlyphs(new CodePoint('H'), out Glyph? glyph)); + + // GetSyntheticBoldStrength receives point size multiplied by DPI. At the default 72 DPI, + // the scale factors cancel to leave the browser-matched emboldening fraction times point size. + float strength = glyph.Value.GlyphMetrics.GetSyntheticBoldStrength(pointSize * 72F); + Assert.Equal(pointSize * ExpectedBoldEmScale, strength, SkewComparer); + } + + [Theory] + [InlineData("OpenSans-Regular.ttf", "TrueType")] + [InlineData("PlantinStdRegular.otf", "CFF")] + public void VisualTest_SyntheticBold(string fontFile, string fontFormat) + { + const string text = "Sphinx of black quartz, judge my vow.\nPack my box with five dozen liquor jugs."; + string path = Path.Combine(TestEnvironment.FontTestDataFullPath, fontFile); + Font font = new(RegularOnlyFamily(path), 36, FontStyle.Bold); + TextOptions options = new(font) + { + Dpi = 96F, + LineSpacing = 1.4F + }; + + // The collection contains only the regular face, so requesting bold exercises the + // synthesized outline while using the standard rendering-test output and comparison path. + TextLayoutTestUtilities.TestLayout(text, options, properties: fontFormat); + } + [Fact] public void SyntheticBold_DilatesGlyphOutline_TrueType() => AssertSyntheticBoldGrows(TestFonts.OpenSansFile, "H"); @@ -208,6 +334,50 @@ public void SyntheticBoldItalic_CombinesBothSyntheses() Assert.True(glyph.Value.GlyphMetrics.ShouldSynthesizeBold()); } + [Fact] + public void SyntheticBold_DoesNotEmboldenPaintedEmoji() + { + FontFamily family = RegularOnlyFamily(TestFonts.TwemojiMozillaFile); + Font regular = new(family, 48, FontStyle.Regular); + Font bold = new(family, 48, FontStyle.Bold); + CodePoint codePoint = new(0x1F600); + + Assert.True(bold.TryGetGlyphs(codePoint, ColorFontSupport.ColrV0, out Glyph? glyph)); + Assert.Equal(GlyphType.Painted, glyph.Value.GlyphMetrics.GlyphType); + Assert.False(glyph.Value.GlyphMetrics.ShouldSynthesizeBold()); + + TextOptions regularOptions = new(regular) + { + HintingMode = HintingMode.None, + ColorFontSupport = ColorFontSupport.ColrV0 + }; + TextOptions boldOptions = new(bold) + { + HintingMode = HintingMode.None, + ColorFontSupport = ColorFontSupport.ColrV0 + }; + + GlyphRenderer regularRenderer = new(); + TextRenderer.RenderTo(regularRenderer, "😀", regularOptions); + + GlyphRenderer boldRenderer = new(); + TextRenderer.RenderTo(boldRenderer, "😀", boldOptions); + + // Browsers leave authored color layers unchanged for bold text. + Assert.Equal(regularRenderer.ControlPoints, boldRenderer.ControlPoints); + Assert.Equal(regularRenderer.GlyphRects, boldRenderer.GlyphRects); + } + + [Fact] + public void VisualTest_SyntheticBoldPaintedEmoji() + { + TextOptions options = PaintedEmojiOptions(FontStyle.Bold); + + // A bold request must preserve the authored color geometry because browsers do not apply + // synthetic emboldening to painted emoji. + TextLayoutTestUtilities.TestLayout("😀 ☺️ ❤️ ✌️ ⭐", options, properties: "COLRv0"); + } + [Fact] public void ShouldSynthesizeBold_ReturnsFalse_WhenGlyphHasNoTextRun() { @@ -267,10 +437,10 @@ private static void AssertSyntheticBoldPreservesSegments(string file, string tex Font bold = new(family, 48, FontStyle.Bold); GlyphRenderer regularRenderer = new(); - TextRenderer.RenderTextTo(regularRenderer, text, new TextOptions(regular) { HintingMode = HintingMode.None }); + TextRenderer.RenderTo(regularRenderer, text, new TextOptions(regular) { HintingMode = HintingMode.None }); GlyphRenderer boldRenderer = new(); - TextRenderer.RenderTextTo(boldRenderer, text, new TextOptions(bold) { HintingMode = HintingMode.None }); + TextRenderer.RenderTo(boldRenderer, text, new TextOptions(bold) { HintingMode = HintingMode.None }); List r = regularRenderer.ControlPoints; List b = boldRenderer.ControlPoints; @@ -316,7 +486,7 @@ public void EmboldeningRenderer_ForwardsCallsAndDilatesOutline() sut.EndFigure(); _ = sut.EnabledDecorations(); - sut.SetDecoration(TextDecorations.Underline, default, default, 1F); + sut.SetDecoration(TextDecorations.Underline, default, default, 1F, ReadOnlyMemory.Empty); sut.EndLayer(); sut.EndGlyph(); sut.EndText(); @@ -336,6 +506,40 @@ public void EmboldeningRenderer_ForwardsCallsAndDilatesOutline() Assert.Equal(8, inner.Figures[0].Count); } + [Fact] + public void EmboldeningRenderer_MatchesFreeTypeForOuterContourAndCounter() + { + RecordingGlyphRenderer inner = new(); + EmboldeningGlyphRenderer sut = new(inner, 2F); + + // The outer contour is clockwise in device coordinates. FreeType leaves its left and top + // edges fixed while increasing the right and bottom extents by the requested strength. + sut.BeginFigure(); + sut.MoveTo(new Vector2(0, 0)); + sut.LineTo(new Vector2(20, 0)); + sut.LineTo(new Vector2(20, 20)); + sut.LineTo(new Vector2(0, 20)); + sut.EndFigure(); + + // The counter uses the opposite winding, so the same outline orientation shrinks it and + // increases the surrounding stem thickness instead of expanding the hole. + sut.BeginFigure(); + sut.MoveTo(new Vector2(5, 5)); + sut.LineTo(new Vector2(5, 15)); + sut.LineTo(new Vector2(15, 15)); + sut.LineTo(new Vector2(15, 5)); + sut.EndFigure(); + + sut.CompleteOutline(); + + Vector2[] expectedOuter = [new(0, 0), new(22, 0), new(22, 22), new(0, 22)]; + Vector2[] expectedCounter = [new(7, 7), new(7, 15), new(15, 15), new(15, 7)]; + + Assert.Equal(2, inner.Figures.Count); + Assert.Equal(expectedOuter, inner.Figures[0]); + Assert.Equal(expectedCounter, inner.Figures[1]); + } + private sealed class RecordingGlyphRenderer : IGlyphRenderer { public bool BeganText { get; private set; } @@ -354,9 +558,9 @@ private sealed class RecordingGlyphRenderer : IGlyphRenderer public bool SetDecorationCalled { get; private set; } - public List> Figures { get; } = new(); + public List> Figures { get; } = []; - private List? current; + private List current; public void BeginText(in FontRectangle bounds) => this.BeganText = true; @@ -370,33 +574,33 @@ public bool BeginGlyph(in FontRectangle bounds, in GlyphRendererParameters param public void EndGlyph() => this.EndedGlyph = true; - public void BeginLayer(Paint? paint, FillRule fillRule, ClipQuad? clipBounds) => this.BeganLayer = true; + public void BeginLayer(Paint paint, FillRule fillRule, ClipQuad? clipBounds) => this.BeganLayer = true; public void EndLayer() => this.EndedLayer = true; - public void BeginFigure() => this.current = new List(); + public void BeginFigure() => this.current = []; - public void MoveTo(Vector2 point) => this.current!.Add(point); + public void MoveTo(Vector2 point) => this.current.Add(point); - public void LineTo(Vector2 point) => this.current!.Add(point); + public void LineTo(Vector2 point) => this.current.Add(point); public void QuadraticBezierTo(Vector2 secondControlPoint, Vector2 point) { - this.current!.Add(secondControlPoint); - this.current!.Add(point); + this.current.Add(secondControlPoint); + this.current.Add(point); } public void CubicBezierTo(Vector2 secondControlPoint, Vector2 thirdControlPoint, Vector2 point) { - this.current!.Add(secondControlPoint); - this.current!.Add(thirdControlPoint); - this.current!.Add(point); + this.current.Add(secondControlPoint); + this.current.Add(thirdControlPoint); + this.current.Add(point); } public void ArcTo(float radiusX, float radiusY, float rotation, bool largeArc, bool sweep, Vector2 point) - => this.current!.Add(point); + => this.current.Add(point); - public void EndFigure() => this.Figures.Add(this.current!); + public void EndFigure() => this.Figures.Add(this.current); public TextDecorations EnabledDecorations() { @@ -404,7 +608,7 @@ public TextDecorations EnabledDecorations() return TextDecorations.None; } - public void SetDecoration(TextDecorations textDecorations, Vector2 start, Vector2 end, float thickness) + public void SetDecoration(TextDecorations textDecorations, Vector2 start, Vector2 end, float thickness, ReadOnlyMemory intersections) => this.SetDecorationCalled = true; } } diff --git a/tests/SixLabors.Fonts.Tests/FontTableDataTests.cs b/tests/SixLabors.Fonts.Tests/FontTableDataTests.cs new file mode 100644 index 000000000..b7ece2706 --- /dev/null +++ b/tests/SixLabors.Fonts.Tests/FontTableDataTests.cs @@ -0,0 +1,122 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.Fonts.Tables.AdvancedTypographic; + +namespace SixLabors.Fonts.Tests; + +public class FontTableDataTests +{ + [Fact] + public void FileBackedFontReturnsTableData() + { + FontMetrics metrics = GetMetrics(new FontCollection().Add(TestFonts.CarterOneFile)); + + Assert.True(metrics.TryGetTableData(Tag.Parse("head"), out ReadOnlyMemory table)); + Assert.False(table.IsEmpty); + } + + [Fact] + public void FileBackedFontReturnsStream() + { + FontMetrics metrics = GetMetrics(new FontCollection().Add(TestFonts.CarterOneFile)); + + using (Stream stream = metrics.OpenStream()) + { + Assert.True(stream.CanRead); + Assert.Equal(0, stream.Position); + } + } + + [Fact] + public void MissingTableReturnsFalse() + { + FontMetrics metrics = GetMetrics(new FontCollection().Add(TestFonts.CarterOneFile)); + + Assert.False(metrics.TryGetTableData(Tag.Parse("ZZZZ"), out ReadOnlyMemory table)); + Assert.True(table.IsEmpty); + } + + [Fact] + public void StreamBackedFontReturnsTableDataAfterSourceDisposed() + { + FontCollection collection = new(); + FontMetrics metrics; + + using (Stream stream = TestFonts.CarterOneFileData()) + { + FontFamily family = collection.Add(stream); + metrics = GetMetrics(family); + } + + Assert.True(metrics.TryGetTableData(Tag.Parse("head"), out ReadOnlyMemory table)); + Assert.False(table.IsEmpty); + } + + [Fact] + public void StreamBackedFontReturnsStreamAfterSourceDisposed() + { + FontCollection collection = new(); + FontMetrics metrics; + + using (Stream source = TestFonts.CarterOneFileData()) + { + FontFamily family = collection.Add(source); + metrics = GetMetrics(family); + } + + using (Stream stream = metrics.OpenStream()) + { + Assert.True(stream.CanRead); + Assert.Equal(0, stream.Position); + } + } + + [Fact] + public void StreamBackedCollectionReturnsTableDataAfterSourceDisposed() + { + FontCollection collection = new(); + FontMetrics metrics; + + using (Stream stream = TestFonts.SSimpleTrueTypeCollectionData()) + { + FontFamily family = Assert.Single(collection.AddCollection(stream).ToArray(), x => x.Name == "Open Sans"); + metrics = GetMetrics(family); + } + + Assert.True(metrics.TryGetTableData(Tag.Parse("head"), out ReadOnlyMemory table)); + Assert.False(table.IsEmpty); + } + + [Fact] + public void WoffTableDataMatchesTrueTypeTableData() + { + ReadOnlyMemory trueTypeTable = GetTableData(TestFonts.OpenSansFile, "head"); + ReadOnlyMemory woffTable = GetTableData(TestFonts.OpenSansFileWoff1, "head"); + + Assert.Equal(trueTypeTable.ToArray(), woffTable.ToArray()); + } + + [Fact] + public void Woff2TableDataMatchesTrueTypeTableData() + { + ReadOnlyMemory trueTypeTable = GetTableData(TestFonts.OpenSansFile, "maxp"); + ReadOnlyMemory woff2Table = GetTableData(TestFonts.OpenSansFileWoff2, "maxp"); + + Assert.Equal(trueTypeTable.ToArray(), woff2Table.ToArray()); + } + + private static ReadOnlyMemory GetTableData(string path, string tag) + { + FontMetrics metrics = GetMetrics(new FontCollection().Add(path)); + + Assert.True(metrics.TryGetTableData(Tag.Parse(tag), out ReadOnlyMemory table)); + return table; + } + + private static FontMetrics GetMetrics(FontFamily family) + { + Assert.True(family.TryGetMetrics(FontStyle.Regular, out FontMetrics metrics)); + return metrics; + } +} diff --git a/tests/SixLabors.Fonts.Tests/FontWeightTests.cs b/tests/SixLabors.Fonts.Tests/FontWeightTests.cs new file mode 100644 index 000000000..37dd48cc3 --- /dev/null +++ b/tests/SixLabors.Fonts.Tests/FontWeightTests.cs @@ -0,0 +1,361 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Globalization; +using SixLabors.Fonts.Rendering; +using SixLabors.Fonts.Unicode; + +namespace SixLabors.Fonts.Tests; + +public class FontWeightTests +{ + private const string BrowserComparisonText = "The quick brown fox jumps over the lazy dog."; + + // The browser page places live browser text and rendered PNGs side by side. These sizes keep + // both columns visible on a typical laptop display while retaining enough detail to compare + // the synthetic and variable weight outlines. + private const float BrowserComparisonPointSize = 18F; + + private const float BrowserComparisonEmojiPointSize = 27F; + + [Fact] + public void TextOptions_DefaultWeightIsNullAndCopyPreservesWeight() + { + Font font = TestFonts.GetFont(TestFonts.OpenSansFile, 12); + TextOptions defaultOptions = new(font); + TextOptions weightedOptions = new(font) { FontWeight = FontWeight.ExtraBold }; + + Assert.Null(defaultOptions.FontWeight); + Assert.Equal(FontWeight.ExtraBold, new TextOptions(weightedOptions).FontWeight); + } + + [Fact] + public void FontDescription_ExposesOpenTypeWeight() + { + FontDescription description = FontDescription.LoadDescription(TestFonts.OpenSansFile); + + Assert.Equal(FontWeight.Normal, description.Weight); + } + + [Fact] + public void TextRunWeight_OverridesTextOptionsWeight() + { + Font font = TestFonts.GetFont(TestFonts.RobotoFlex, 12); + TextRun textRun = new() + { + Start = 0, + End = 1, + FontWeight = FontWeight.Black + }; + + TextOptions options = new(font) + { + FontWeight = FontWeight.Light, + TextRuns = [textRun] + }; + + TextRun resolvedRun = Assert.Single(TextLayout.BuildTextRuns("A", options)); + FontVariation weight = Assert.Single( + resolvedRun.ResolvedFont.Variations.ToArray(), + variation => variation.Tag == KnownVariationAxes.Weight); + + Assert.Equal((int)FontWeight.Black, weight.Value); + } + + [Fact] + public void VariableWeight_MatchesExplicitVariation() + { + Font font = TestFonts.GetFont(TestFonts.RobotoFlex, 48); + Font explicitFont = new(font, new FontVariation(KnownVariationAxes.Weight, (int)FontWeight.Black)); + + GlyphRenderer weightedRenderer = new(); + TextRenderer.RenderTo( + weightedRenderer, + "Weight", + new TextOptions(font) { FontWeight = FontWeight.Black }); + + GlyphRenderer explicitRenderer = new(); + TextRenderer.RenderTo(explicitRenderer, "Weight", new TextOptions(explicitFont)); + + Assert.Equal(explicitRenderer.ControlPoints, weightedRenderer.ControlPoints); + Assert.Equal(explicitRenderer.GlyphRects, weightedRenderer.GlyphRects); + } + + [Theory] + [InlineData(FontWeight.Normal, 0F)] + [InlineData(FontWeight.Medium, 0F)] + [InlineData(FontWeight.SemiBold, 1F)] + [InlineData(FontWeight.Bold, 1F)] + [InlineData(FontWeight.Black, 1F)] + public void StaticWeight_UsesBrowserBoldThreshold(FontWeight weight, float expectedFactor) + { + const float pointSize = 48F; + Font font = new(RegularOnlyFamily(TestFonts.OpenSansFile), pointSize); + TextRun textRun = new() + { + Start = 0, + End = 1, + Font = font, + FontWeight = weight + }; + + textRun.ResolveFontWeight(defaultWeight: null); + Assert.True(font.FontMetrics.TryGetGlyphMetrics( + new CodePoint('H'), + TextAttributes.None, + TextDecorations.None, + LayoutMode.HorizontalTopBottom, + ColorFontSupport.None, + out FontGlyphMetrics metrics)); + + FontGlyphMetrics renderMetrics = metrics.CloneForRendering(textRun); + float strength = renderMetrics.GetSyntheticBoldStrength(pointSize * 72F); + + Assert.Equal((pointSize / 31F) * expectedFactor, strength, new ApproximateFloatComparer(.0001F)); + } + + [Theory] + [InlineData(FontWeight.Medium)] + [InlineData(FontWeight.Bold)] + [InlineData(FontWeight.Black)] + public void StaticWeight_DoesNotChangeAdvance(FontWeight weight) + { + Font font = new(RegularOnlyFamily(TestFonts.OpenSansFile), 48); + FontRectangle regularAdvance = TextMeasurer.MeasureAdvance("Weight", new TextOptions(font)); + FontRectangle weightedAdvance = TextMeasurer.MeasureAdvance( + "Weight", + new TextOptions(font) { FontWeight = weight }); + + Assert.Equal(regularAdvance.Width, weightedAdvance.Width); + } + + [Fact] + public void PaintedEmoji_DoesNotSynthesizeWeight() + { + Font font = new(RegularOnlyFamily(TestFonts.TwemojiMozillaFile), 48); + TextRun textRun = new() + { + Start = 0, + End = 1, + Font = font, + FontWeight = FontWeight.Black + }; + + textRun.ResolveFontWeight(defaultWeight: null); + Assert.True(font.FontMetrics.TryGetGlyphMetrics( + new CodePoint(0x1F600), + TextAttributes.None, + TextDecorations.None, + LayoutMode.HorizontalTopBottom, + ColorFontSupport.ColrV0, + out FontGlyphMetrics metrics)); + + FontGlyphMetrics renderMetrics = metrics.CloneForRendering(textRun); + + Assert.Equal(GlyphType.Painted, renderMetrics.GlyphType); + Assert.False(renderMetrics.ShouldSynthesizeBold()); + } + + [Theory] + [InlineData(FontWeight.Medium)] + [InlineData(FontWeight.SemiBold)] + public void SystemWeight_UsesInstalledFaceWhenAvailable(FontWeight requestedWeight) + { + if (!TestEnvironment.IsWindows || !SystemFonts.TryGet("Segoe UI", out FontFamily family)) + { + return; + } + + Font font = family.CreateFont(18); + TextOptions options = new(font) { FontWeight = requestedWeight }; + TextRun textRun = Assert.Single(TextLayout.BuildTextRuns("Weight", options)); + + // DirectWrite resolves both 500 and 600 to Segoe UI Semibold. This specifically protects + // the equal-distance 500 request from incorrectly selecting the lighter Regular 400 face. + Assert.Equal(FontWeight.SemiBold, textRun.ResolvedFont.FontMetrics.Description.Weight); + Assert.False(textRun.UsesVariableWeight); + + Assert.True(textRun.ResolvedFont.FontMetrics.TryGetGlyphMetrics( + new CodePoint('W'), + TextAttributes.None, + TextDecorations.None, + LayoutMode.HorizontalTopBottom, + ColorFontSupport.None, + out FontGlyphMetrics metrics)); + + Assert.False(metrics.CloneForRendering(textRun).ShouldSynthesizeBold()); + } + + [Theory] + [InlineData(FontWeight.Thin, FontWeight.Normal)] + [InlineData(FontWeight.ExtraLight, FontWeight.Normal)] + [InlineData(FontWeight.Light, FontWeight.Normal)] + [InlineData(FontWeight.Normal, FontWeight.Normal)] + [InlineData(FontWeight.Medium, FontWeight.Normal)] + [InlineData(FontWeight.SemiBold, FontWeight.Bold)] + [InlineData(FontWeight.Bold, FontWeight.Bold)] + [InlineData(FontWeight.ExtraBold, FontWeight.Black)] + [InlineData(FontWeight.Black, FontWeight.Black)] + public void SystemWeight_UsesBrowserFaceMatching(FontWeight requestedWeight, FontWeight expectedWeight) + { + if (!TestEnvironment.IsWindows || !SystemFonts.TryGet("Arial", out FontFamily family)) + { + return; + } + + Font font = family.CreateFont(18); + TextOptions options = new(font) { FontWeight = requestedWeight }; + TextRun textRun = Assert.Single(TextLayout.BuildTextRuns("Weight", options)); + + // CSS Fonts Level 4 section 5 maps missing weights to the nearest face in a specified + // search direction. For the installed Arial family, 100-500 select Regular, 600-700 + // select Bold, and 800-900 select Arial Black. + Assert.Equal(expectedWeight, textRun.ResolvedFont.FontMetrics.Description.Weight); + Assert.False(textRun.UsesVariableWeight); + + Assert.True(textRun.ResolvedFont.FontMetrics.TryGetGlyphMetrics( + new CodePoint('W'), + TextAttributes.None, + TextDecorations.None, + LayoutMode.HorizontalTopBottom, + ColorFontSupport.None, + out FontGlyphMetrics metrics)); + + Assert.False(metrics.CloneForRendering(textRun).ShouldSynthesizeBold()); + } + + [Theory] + [InlineData(FontWeight.Thin, "Thin")] + [InlineData(FontWeight.ExtraLight, "ExtraLight")] + [InlineData(FontWeight.Light, "Light")] + [InlineData(FontWeight.Normal, "Normal")] + [InlineData(FontWeight.Medium, "Medium")] + [InlineData(FontWeight.SemiBold, "SemiBold")] + [InlineData(FontWeight.Bold, "Bold")] + [InlineData(FontWeight.ExtraBold, "ExtraBold")] + [InlineData(FontWeight.Black, "Black")] + public void VisualTest_StaticWeight(FontWeight weight, string label) + { + Font font = new(RegularOnlyFamily(TestFonts.OpenSansFile), BrowserComparisonPointSize); + TextOptions options = BrowserComparisonOptions(font, weight); + + // Open Sans exposes only its regular face here, matching the browser's single-face + // declaration and exercising synthetic weight above the authored 400 weight. + TextLayoutTestUtilities.TestLayout( + BrowserComparisonText, + options, + properties: [label, (int)weight]); + } + + [Theory] + [InlineData(FontWeight.Thin, "Thin")] + [InlineData(FontWeight.ExtraLight, "ExtraLight")] + [InlineData(FontWeight.Light, "Light")] + [InlineData(FontWeight.Normal, "Normal")] + [InlineData(FontWeight.Medium, "Medium")] + [InlineData(FontWeight.SemiBold, "SemiBold")] + [InlineData(FontWeight.Bold, "Bold")] + [InlineData(FontWeight.ExtraBold, "ExtraBold")] + [InlineData(FontWeight.Black, "Black")] + public void VisualTest_VariableWeight(FontWeight weight, string label) + { + Font font = TestFonts.GetFont(TestFonts.RobotoFlex, BrowserComparisonPointSize); + TextOptions options = BrowserComparisonOptions(font, weight); + + // Roboto Flex exposes a registered wght axis, matching the browser's variable @font-face. + TextLayoutTestUtilities.TestLayout( + BrowserComparisonText, + options, + properties: [label, (int)weight]); + } + + [Theory] + [InlineData(FontWeight.Thin, "Thin")] + [InlineData(FontWeight.ExtraLight, "ExtraLight")] + [InlineData(FontWeight.Light, "Light")] + [InlineData(FontWeight.Normal, "Normal")] + [InlineData(FontWeight.Medium, "Medium")] + [InlineData(FontWeight.SemiBold, "SemiBold")] + [InlineData(FontWeight.Bold, "Bold")] + [InlineData(FontWeight.ExtraBold, "ExtraBold")] + [InlineData(FontWeight.Black, "Black")] + public void VisualTest_SystemSegoeUIWeight(FontWeight weight, string label) + { + if (!TestEnvironment.IsWindows || !SystemFonts.TryGet("Segoe UI", out FontFamily family)) + { + return; + } + + TextOptions options = BrowserComparisonOptions(family.CreateFont(BrowserComparisonPointSize), weight); + + // The browser and Fonts both resolve this family through the Windows system collection, + // allowing authored numeric faces and synthesized missing weights to be compared directly. + TextLayoutTestUtilities.TestLayout( + BrowserComparisonText, + options, + properties: [label, (int)weight]); + } + + [Theory] + [InlineData(FontWeight.Thin, "Thin")] + [InlineData(FontWeight.ExtraLight, "ExtraLight")] + [InlineData(FontWeight.Light, "Light")] + [InlineData(FontWeight.Normal, "Normal")] + [InlineData(FontWeight.Medium, "Medium")] + [InlineData(FontWeight.SemiBold, "SemiBold")] + [InlineData(FontWeight.Bold, "Bold")] + [InlineData(FontWeight.ExtraBold, "ExtraBold")] + [InlineData(FontWeight.Black, "Black")] + public void VisualTest_SystemArialWeight(FontWeight weight, string label) + { + if (!TestEnvironment.IsWindows || !SystemFonts.TryGet("Arial", out FontFamily family)) + { + return; + } + + TextOptions options = BrowserComparisonOptions(family.CreateFont(BrowserComparisonPointSize), weight); + + // Arial provides the common regular and bold faces but not every numeric weight, exposing + // how the browser and Fonts each handle weights missing from an installed static family. + TextLayoutTestUtilities.TestLayout( + BrowserComparisonText, + options, + properties: [label, (int)weight]); + } + + [Theory] + [InlineData(FontWeight.Normal, "Normal")] + [InlineData(FontWeight.Black, "Black")] + public void VisualTest_PaintedEmojiWeight(FontWeight weight, string label) + { + FontCollection collection = new(); + FontFamily emoji = collection.Add(TestFonts.TwemojiMozillaFile); + FontFamily fallback = collection.Add(TestFonts.OpenSansFile); + TextOptions options = BrowserComparisonOptions(new Font(emoji, BrowserComparisonEmojiPointSize), weight); + options.ColorFontSupport = ColorFontSupport.ColrV0; + options.FallbackFontFamilies = [fallback]; + + // The 900 sample must retain the same authored color geometry as the 400 sample. + TextLayoutTestUtilities.TestLayout( + "😀 ☺️ ❤️ ✌️ ⭐", + options, + properties: [label, (int)weight]); + } + + private static TextOptions BrowserComparisonOptions(Font font, FontWeight weight) + => new(font) + { + FontWeight = weight, + Dpi = 96F, + LineSpacing = 1.4F + }; + + private static FontFamily RegularOnlyFamily(string path) + { + const string familyName = "Weight Test"; + FontCollection collection = new(); + IFontMetricsCollection metricsCollection = collection; + metricsCollection.AddMetrics(new FileFontMetrics(path), familyName, FontStyle.Regular); + return collection.GetByCulture(familyName, CultureInfo.InvariantCulture); + } +} diff --git a/tests/SixLabors.Fonts.Tests/GlyphRenderer.cs b/tests/SixLabors.Fonts.Tests/GlyphRenderer.cs index 3766046ca..ae11cedbb 100644 --- a/tests/SixLabors.Fonts.Tests/GlyphRenderer.cs +++ b/tests/SixLabors.Fonts.Tests/GlyphRenderer.cs @@ -18,6 +18,8 @@ public class GlyphRenderer : IGlyphRenderer public List GlyphKeys { get; } = []; + public List<(TextDecorations Decoration, Vector2 Start, Vector2 End, float Thickness)> Decorations { get; } = []; + public int FiguresCount { get; set; } public bool BeginGlyph(in FontRectangle bounds, in GlyphRendererParameters parameters) @@ -81,9 +83,8 @@ public void BeginText(in FontRectangle bounds) public TextDecorations EnabledDecorations() => this.parameters.TextRun.TextDecorations; - public void SetDecoration(TextDecorations textDecorations, Vector2 start, Vector2 end, float thickness) - { - } + public void SetDecoration(TextDecorations textDecorations, Vector2 start, Vector2 end, float thickness, ReadOnlyMemory intersections) + => this.Decorations.Add((textDecorations, start, end, thickness)); public virtual void BeginLayer(Paint paint, FillRule fillRule, ClipQuad? clipBounds) { diff --git a/tests/SixLabors.Fonts.Tests/GlyphTests.cs b/tests/SixLabors.Fonts.Tests/GlyphTests.cs index aec0cf2a9..ee873be91 100644 --- a/tests/SixLabors.Fonts.Tests/GlyphTests.cs +++ b/tests/SixLabors.Fonts.Tests/GlyphTests.cs @@ -48,7 +48,7 @@ public void RenderToPointAndSingleDPI() 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, 0, locationInFontSpace, Vector2.Zero, GlyphLayoutMode.Horizontal, new TextOptions(font)); + glyph.RenderTo(this.renderer, 0, locationInFontSpace, Vector2.Zero, new Vector2(-1F), GlyphLayoutMode.Horizontal, new TextOptions(font)); Assert.Equal(new FontRectangle(99, 89, 0, 0), this.renderer.GlyphRects.Single()); } @@ -59,7 +59,7 @@ public void IdenticalGlyphsInDifferentPlacesCreateDifferentKeys() Font fakeFont = CreateFont("AB"); TextRenderer textRenderer = new(this.renderer); - textRenderer.RenderText("ABA", new TextOptions(fakeFont)); + textRenderer.Render("ABA", new TextOptions(fakeFont)); Assert.NotEqual(this.renderer.GlyphKeys[0], this.renderer.GlyphKeys[2]); Assert.NotEqual(this.renderer.GlyphKeys[1], this.renderer.GlyphKeys[2]); @@ -73,7 +73,7 @@ public void BeginGlyph_ReturnsFalse_SkipRenderingFigures() Font fakeFont = CreateFont("A"); TextRenderer textRenderer = new(renderer.Object); - textRenderer.RenderText("ABA", new TextOptions(fakeFont)); + textRenderer.Render("ABA", new TextOptions(fakeFont)); renderer.Verify(x => x.BeginFigure(), Times.Never); } @@ -85,7 +85,7 @@ public void BeginGlyph_ReturnsTrue_RendersFigures() Font fakeFont = CreateFont("A"); TextRenderer textRenderer = new(renderer.Object); - textRenderer.RenderText("ABA", new TextOptions(fakeFont)); + textRenderer.Render("ABA", new TextOptions(fakeFont)); renderer.Verify(x => x.BeginFigure(), Times.Exactly(3)); } @@ -114,7 +114,7 @@ public void RenderColrGlyphTextRenderer() Font font = TestFonts.GetFont(TestFonts.TwemojiMozillaFile, 12); ColorGlyphRenderer renderer = new(); - TextRenderer.RenderTextTo(renderer, "😀", new TextOptions(font) + TextRenderer.RenderTo(renderer, "😀", new TextOptions(font) { ColorFontSupport = ColorFontSupport.ColrV0 }); @@ -130,7 +130,7 @@ public void RenderColrGlyphWithVariationSelector() const string text = "\u263A\uFE0F"; // Fully-qualified sequence for emoji 'smiling face' ColorGlyphRenderer renderer = new(); - TextRenderer.RenderTextTo(renderer, text, new TextOptions(font) + TextRenderer.RenderTo(renderer, text, new TextOptions(font) { ColorFontSupport = ColorFontSupport.ColrV0 }); @@ -152,8 +152,8 @@ public void EmojiWidthIsComputedCorrectlyWithSubstitutionOnZwj() FontRectangle size = TextMeasurer.MeasureBounds(text, new TextOptions(font)); FontRectangle size2 = TextMeasurer.MeasureBounds(text2, new TextOptions(font)); - Assert.Equal(55.652F, size.Width, Comparer); - Assert.Equal(55.617F, size2.Width, Comparer); + Assert.Equal(50.625F, size.Width, Comparer); + Assert.Equal(50.555F, size2.Width, Comparer); } [Theory] @@ -168,14 +168,14 @@ public void RenderWoffGlyphs_IsEqualToTtfGlyphs(bool applyKerning, bool applyHin string testStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; ColorGlyphRenderer rendererTtf = new(); - TextRenderer.RenderTextTo(rendererTtf, testStr, new TextOptions(fontTtf) + TextRenderer.RenderTo(rendererTtf, testStr, new TextOptions(fontTtf) { KerningMode = applyKerning ? KerningMode.Standard : KerningMode.None, HintingMode = applyHinting ? HintingMode.Standard : HintingMode.None, ColorFontSupport = ColorFontSupport.ColrV0 }); ColorGlyphRenderer rendererWoff = new(); - TextRenderer.RenderTextTo(rendererWoff, testStr, new TextOptions(fontWoff) + TextRenderer.RenderTo(rendererWoff, testStr, new TextOptions(fontWoff) { KerningMode = applyKerning ? KerningMode.Standard : KerningMode.None, HintingMode = applyHinting ? HintingMode.Standard : HintingMode.None, @@ -198,13 +198,13 @@ public void RenderWoff_CompositeGlyphs_IsEqualToTtfGlyphs(string testStr) Font fontWoff = TestFonts.GetFont(TestFonts.OpenSansFileWoff1, 12); ColorGlyphRenderer rendererTtf = new(); - TextRenderer.RenderTextTo(rendererTtf, testStr, new TextOptions(fontTtf) + TextRenderer.RenderTo(rendererTtf, testStr, new TextOptions(fontTtf) { HintingMode = HintingMode.Standard, ColorFontSupport = ColorFontSupport.ColrV0 }); ColorGlyphRenderer rendererWoff = new(); - TextRenderer.RenderTextTo(rendererWoff, testStr, new TextOptions(fontWoff) + TextRenderer.RenderTo(rendererWoff, testStr, new TextOptions(fontWoff) { HintingMode = HintingMode.Standard, ColorFontSupport = ColorFontSupport.ColrV0 @@ -226,14 +226,14 @@ public void RenderWoff2Glyphs_IsEqualToTtfGlyphs(bool applyKerning, bool applyHi string testStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; ColorGlyphRenderer rendererTtf = new(); - TextRenderer.RenderTextTo(rendererTtf, testStr, new TextOptions(fontTtf) + TextRenderer.RenderTo(rendererTtf, testStr, new TextOptions(fontTtf) { KerningMode = applyKerning ? KerningMode.Standard : KerningMode.None, HintingMode = applyHinting ? HintingMode.Standard : HintingMode.None, ColorFontSupport = ColorFontSupport.ColrV0 }); ColorGlyphRenderer rendererWoff2 = new(); - TextRenderer.RenderTextTo(rendererWoff2, testStr, new TextOptions(fontWoff2) + TextRenderer.RenderTo(rendererWoff2, testStr, new TextOptions(fontWoff2) { KerningMode = applyKerning ? KerningMode.Standard : KerningMode.None, HintingMode = applyHinting ? HintingMode.Standard : HintingMode.None, @@ -256,13 +256,13 @@ public void RenderWoff2_CompositeGlyphs_IsEqualToTtfGlyphs(string testStr) Font fontWoff2 = TestFonts.GetFont(TestFonts.OpenSansFileWoff2, 12); ColorGlyphRenderer rendererTtf = new(); - TextRenderer.RenderTextTo(rendererTtf, testStr, new TextOptions(fontTtf) + TextRenderer.RenderTo(rendererTtf, testStr, new TextOptions(fontTtf) { HintingMode = HintingMode.Standard, ColorFontSupport = ColorFontSupport.ColrV0 }); ColorGlyphRenderer rendererWoff2 = new(); - TextRenderer.RenderTextTo(rendererWoff2, testStr, new TextOptions(fontWoff2) + TextRenderer.RenderTo(rendererWoff2, testStr, new TextOptions(fontWoff2) { HintingMode = HintingMode.Standard, ColorFontSupport = ColorFontSupport.ColrV0 @@ -282,10 +282,10 @@ public void RendererIsThreadsafe(string fontName) Parallel.For(0, threadCount, _ => { ColorGlyphRenderer renderer1 = new(); - TextRenderer.RenderTextTo(renderer1, "A 🙂 ", new TextOptions(SystemFonts.CreateFont(fontName, 15))); + TextRenderer.RenderTo(renderer1, "A 🙂 ", new TextOptions(SystemFonts.CreateFont(fontName, 15))); ColorGlyphRenderer renderer2 = new(); - TextRenderer.RenderTextTo(renderer2, "A 🙂 ", new TextOptions(SystemFonts.CreateFont(fontName, 15))); + TextRenderer.RenderTo(renderer2, "A 🙂 ", new TextOptions(SystemFonts.CreateFont(fontName, 15))); Assert.True(renderer1.ControlPoints.Count > 0); Assert.True(renderer2.ControlPoints.Count > 0); diff --git a/tests/SixLabors.Fonts.Tests/HintingTests.cs b/tests/SixLabors.Fonts.Tests/HintingTests.cs index f4d4570c0..ab60e5aae 100644 --- a/tests/SixLabors.Fonts.Tests/HintingTests.cs +++ b/tests/SixLabors.Fonts.Tests/HintingTests.cs @@ -114,7 +114,7 @@ static void RenderTo(FontFamily family, string text, float size, float dpi, Glyp HintingMode = HintingMode.Standard, }; - TextRenderer.RenderTextTo(renderer, text, options); + TextRenderer.RenderTo(renderer, text, options); } // Render the target size on a font whose interpreter has processed nothing else. diff --git a/tests/SixLabors.Fonts.Tests/Issues/Issues_23.cs b/tests/SixLabors.Fonts.Tests/Issues/Issues_23.cs index 0c5a34251..db5aec24c 100644 --- a/tests/SixLabors.Fonts.Tests/Issues/Issues_23.cs +++ b/tests/SixLabors.Fonts.Tests/Issues/Issues_23.cs @@ -15,7 +15,7 @@ public void BleedingFonts() GlyphRenderer r = new(); - new TextRenderer(r).RenderText("o", new TextOptions(new Font(font, 30))); + new TextRenderer(r).Render("o", new TextOptions(new Font(font, 30))); Assert.DoesNotContain(System.Numerics.Vector2.Zero, r.ControlPoints); } diff --git a/tests/SixLabors.Fonts.Tests/Issues/Issues_334.cs b/tests/SixLabors.Fonts.Tests/Issues/Issues_334.cs index aacbc318e..56289eb62 100644 --- a/tests/SixLabors.Fonts.Tests/Issues/Issues_334.cs +++ b/tests/SixLabors.Fonts.Tests/Issues/Issues_334.cs @@ -15,7 +15,7 @@ public void DoesNotSkewCompositeGlyph() Font font = family.CreateFont(metrics.UnitsPerEm); ColorGlyphRenderer renderer = new(); - TextRenderer.RenderTextTo(renderer, "(", new TextOptions(font)); + TextRenderer.RenderTo(renderer, "(", new TextOptions(font)); Assert.Single(renderer.GlyphRects); diff --git a/tests/SixLabors.Fonts.Tests/Issues/Issues_337.cs b/tests/SixLabors.Fonts.Tests/Issues/Issues_337.cs index b0a19daca..1aba47348 100644 --- a/tests/SixLabors.Fonts.Tests/Issues/Issues_337.cs +++ b/tests/SixLabors.Fonts.Tests/Issues/Issues_337.cs @@ -17,7 +17,7 @@ public void CanShapeCompositeGlyphs() HintingMode = HintingMode.Standard }; - TextRenderer.RenderTextTo(renderer, "標楷體輸出", options); + TextRenderer.RenderTo(renderer, "標楷體輸出", options); Assert.Equal(5, renderer.GlyphKeys.Count); Assert.Equal(5, renderer.GlyphRects.Count); diff --git a/tests/SixLabors.Fonts.Tests/Issues/Issues_383.cs b/tests/SixLabors.Fonts.Tests/Issues/Issues_383.cs index 683a87576..d4edfd800 100644 --- a/tests/SixLabors.Fonts.Tests/Issues/Issues_383.cs +++ b/tests/SixLabors.Fonts.Tests/Issues/Issues_383.cs @@ -22,27 +22,27 @@ public void CanBreakLinesWithShortWrappingLength() }; // OK - TextRenderer.RenderTextTo(new NoOpGlyphRenderer(), "i", textOption); + TextRenderer.RenderTo(new NoOpGlyphRenderer(), "i", textOption); // OK - TextRenderer.RenderTextTo(new NoOpGlyphRenderer(), "v", textOption); + TextRenderer.RenderTo(new NoOpGlyphRenderer(), "v", textOption); // raise ArgumentOutOfRangeException - TextRenderer.RenderTextTo(new NoOpGlyphRenderer(), "a", textOption); + TextRenderer.RenderTo(new NoOpGlyphRenderer(), "a", textOption); textOption.WrappingLength = 9.0F; // OK - TextRenderer.RenderTextTo(new NoOpGlyphRenderer(), "i", textOption); + TextRenderer.RenderTo(new NoOpGlyphRenderer(), "i", textOption); // raise ArgumentOutOfRangeException - TextRenderer.RenderTextTo(new NoOpGlyphRenderer(), "v", textOption); + TextRenderer.RenderTo(new NoOpGlyphRenderer(), "v", textOption); // OK - TextRenderer.RenderTextTo(new NoOpGlyphRenderer(), "i\r\nv", textOption); + TextRenderer.RenderTo(new NoOpGlyphRenderer(), "i\r\nv", textOption); // raise ArgumentOutOfRangeException - TextRenderer.RenderTextTo(new NoOpGlyphRenderer(), "v\r\ni", textOption); + TextRenderer.RenderTo(new NoOpGlyphRenderer(), "v\r\ni", textOption); } } @@ -92,7 +92,7 @@ public void QuadraticBezierTo(Vector2 secondControlPoint, Vector2 point) { } - public void SetDecoration(TextDecorations textDecorations, Vector2 start, Vector2 end, float thickness) + public void SetDecoration(TextDecorations textDecorations, Vector2 start, Vector2 end, float thickness, ReadOnlyMemory intersections) { } diff --git a/tests/SixLabors.Fonts.Tests/Issues/Issues_39.cs b/tests/SixLabors.Fonts.Tests/Issues/Issues_39.cs index aa8d1ae5f..7926612b1 100644 --- a/tests/SixLabors.Fonts.Tests/Issues/Issues_39.cs +++ b/tests/SixLabors.Fonts.Tests/Issues/Issues_39.cs @@ -15,7 +15,7 @@ public void RenderingEmptyString_DoesNotThrow() Font font = CreateFont("\t x"); GlyphRenderer r = new(); - new TextRenderer(r).RenderText(string.Empty, new TextOptions(new Font(font, 30))); + new TextRenderer(r).Render(string.Empty, new TextOptions(new Font(font, 30))); } public static Font CreateFont(string text) diff --git a/tests/SixLabors.Fonts.Tests/Issues/Issues_417.cs b/tests/SixLabors.Fonts.Tests/Issues/Issues_417.cs index 073a2f750..001f37b4a 100644 --- a/tests/SixLabors.Fonts.Tests/Issues/Issues_417.cs +++ b/tests/SixLabors.Fonts.Tests/Issues/Issues_417.cs @@ -28,7 +28,7 @@ public void DoesNotThrow_InvalidAnchor() Assert.Equal(361, glyphs[3].Advance.Width); GlyphRenderer renderer = new(); - TextRenderer.RenderTextTo(renderer, "Text", new TextOptions(font)); + TextRenderer.RenderTo(renderer, "Text", new TextOptions(font)); int[] expectedGlyphIndices = [55, 72, 91, 87]; Assert.Equal(expectedGlyphIndices.Length, renderer.GlyphKeys.Count); diff --git a/tests/SixLabors.Fonts.Tests/Issues/Issues_429.cs b/tests/SixLabors.Fonts.Tests/Issues/Issues_429.cs index 979214328..ec64b54ce 100644 --- a/tests/SixLabors.Fonts.Tests/Issues/Issues_429.cs +++ b/tests/SixLabors.Fonts.Tests/Issues/Issues_429.cs @@ -23,7 +23,7 @@ public void VerticalMixedLayout_ExpectedRotation() }; GlyphRenderer renderer = new(); - TextRenderer.RenderTextTo(renderer, text, options); + TextRenderer.RenderTo(renderer, text, options); // Only the Latin glyph + space should be rotated. // Any other glyphs that appear rotated have actually been substituted by the font. diff --git a/tests/SixLabors.Fonts.Tests/Issues/Issues_462.cs b/tests/SixLabors.Fonts.Tests/Issues/Issues_462.cs index e3dddc789..fc9f1573b 100644 --- a/tests/SixLabors.Fonts.Tests/Issues/Issues_462.cs +++ b/tests/SixLabors.Fonts.Tests/Issues/Issues_462.cs @@ -35,7 +35,7 @@ public void CanRenderEmojiFont_With_COLRv1() }; GlyphRenderer renderer = new(); - TextRenderer.RenderTextTo(renderer, text, options); + TextRenderer.RenderTo(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 @@ -70,7 +70,7 @@ public void CanRenderEmojiFont_With_SVG() }; GlyphRenderer renderer = new(); - TextRenderer.RenderTextTo(renderer, text, options); + TextRenderer.RenderTo(renderer, text, options); Assert.Equal(10, renderer.GlyphKeys.Count); TextLayoutTestUtilities.TestLayout( @@ -118,7 +118,7 @@ public void Svg_UsesDefaultBlackFillForUnspecifiedCatFaceDetails() }; LayerCaptureRenderer renderer = new(); - TextRenderer.RenderTextTo(renderer, "😸", options); + TextRenderer.RenderTo(renderer, "😸", options); Assert.Single(renderer.GlyphKeys); Assert.True(renderer.SolidLayers.Count(x => x.Color == GlyphColor.Black && Math.Abs(x.Opacity - 1F) < 0.001F) >= 9); @@ -136,7 +136,7 @@ public void Svg_PropagatesUseOpacityToReferencedGeometry() }; LayerCaptureRenderer renderer = new(); - TextRenderer.RenderTextTo(renderer, "🧐", options); + TextRenderer.RenderTo(renderer, "🧐", options); Assert.Single(renderer.GlyphKeys); Assert.True(GlyphColor.TryParseHex("#CCCCCC", out GlyphColor monocleColor)); @@ -158,7 +158,7 @@ private void AssertCanRenderProblemEmojiTransforms( }; GlyphRenderer renderer = new(); - TextRenderer.RenderTextTo(renderer, text, options); + TextRenderer.RenderTo(renderer, text, options); Assert.Single(renderer.GlyphKeys); TextLayoutTestUtilities.TestLayout(text, options, test: test, properties: name); @@ -199,7 +199,7 @@ private void AssertCanRenderEmojiSanityMatrix( }; GlyphRenderer renderer = new(); - TextRenderer.RenderTextTo(renderer, text, options); + TextRenderer.RenderTo(renderer, text, options); Assert.NotEmpty(renderer.GlyphKeys); TextLayoutTestUtilities.TestLayout(text, options, test: test, properties: "full-string"); diff --git a/tests/SixLabors.Fonts.Tests/Issues/Issues_475.cs b/tests/SixLabors.Fonts.Tests/Issues/Issues_475.cs index 6d3901c75..1662b172e 100644 --- a/tests/SixLabors.Fonts.Tests/Issues/Issues_475.cs +++ b/tests/SixLabors.Fonts.Tests/Issues/Issues_475.cs @@ -28,7 +28,7 @@ public void Test_Issue_475() // None of the fonts here can actually render the real glyphs in the text, just squares // so just verify that we don't hit any exceptions and get the correct glyph count. GlyphRenderer renderer = new(); - TextRenderer.RenderTextTo(renderer, text, options); + TextRenderer.RenderTo(renderer, text, options); Assert.Equal(43, renderer.GlyphRects.Count); } diff --git a/tests/SixLabors.Fonts.Tests/Native/MacSystemFontsEnumeratorTests.cs b/tests/SixLabors.Fonts.Tests/Native/MacSystemFontsEnumeratorTests.cs deleted file mode 100644 index 75e367bbf..000000000 --- a/tests/SixLabors.Fonts.Tests/Native/MacSystemFontsEnumeratorTests.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Runtime.InteropServices; -using SixLabors.Fonts.Native; - -namespace SixLabors.Fonts.Tests.Native; - -public class MacSystemFontsEnumeratorTests -{ - [Fact] - public void TestReset() - { - if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - { - return; - } - - using MacSystemFontsEnumerator enumerator = new(); - HashSet fonts1 = new(enumerator); - Assert.NotEmpty(fonts1); - - enumerator.Reset(); - HashSet fonts2 = new(enumerator); - Assert.Empty(fonts1.Except(fonts2)); - } -} diff --git a/tests/SixLabors.Fonts.Tests/SixLabors.Fonts.Tests.csproj b/tests/SixLabors.Fonts.Tests/SixLabors.Fonts.Tests.csproj index d9d7b9500..afef4585d 100644 --- a/tests/SixLabors.Fonts.Tests/SixLabors.Fonts.Tests.csproj +++ b/tests/SixLabors.Fonts.Tests/SixLabors.Fonts.Tests.csproj @@ -28,7 +28,8 @@ 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 diff --git a/tests/SixLabors.Fonts.Tests/SystemFontCollectionTests.cs b/tests/SixLabors.Fonts.Tests/SystemFontCollectionTests.cs index 6077ec208..6b4bec077 100644 --- a/tests/SixLabors.Fonts.Tests/SystemFontCollectionTests.cs +++ b/tests/SixLabors.Fonts.Tests/SystemFontCollectionTests.cs @@ -2,7 +2,9 @@ // Licensed under the Six Labors Split License. using System.Collections; +using System.Globalization; using System.Runtime.InteropServices; +using SixLabors.Fonts.Unicode; namespace SixLabors.Fonts.Tests; @@ -17,6 +19,36 @@ public void SystemFonts_IsPopulated() Assert.Equal(SystemFonts.Collection.Families, SystemFonts.Families); } + [Fact] + public void SystemFonts_CanGetFamilyNames() + { + string[] familyNames = SystemFonts.GetFamilyNames(); + + Assert.NotEmpty(familyNames); + Assert.All(familyNames, name => Assert.False(string.IsNullOrWhiteSpace(name))); + Assert.Equal( + SystemFonts.Families.Select(x => x.Name).OrderBy(x => x, StringComparer.OrdinalIgnoreCase), + familyNames.OrderBy(x => x, StringComparer.OrdinalIgnoreCase)); + } + + [Fact] + public void SystemFonts_CanResolveFontFromFamilyNames() + { + string familyName = SystemFonts.GetFamilyNames().First(); + + Assert.True(SystemFonts.TryGet(familyName, out FontFamily family)); + Assert.False(family.GetAvailableStyles().IsEmpty); + } + + [Fact] + public void SystemFonts_CanGetDefaultFamilyName() + { + string familyName = SystemFonts.GetDefaultFamilyName(); + + Assert.False(string.IsNullOrWhiteSpace(familyName)); + Assert.NotNull(SystemFonts.CreateFont(familyName, 12F)); + } + [Fact] public void SystemFonts_CanGetFont() { @@ -67,6 +99,52 @@ public void SystemFonts_CanCreateFont_WithCulture() Assert.NotNull(font); } + [Fact] + public void SystemFonts_TryMatchCharacter_Windows_MatchesEmojiFallback() + { + if (!TestEnvironment.IsWindows) + { + return; + } + + CodePoint codePoint = new(0x1F600); + + Assert.True(SystemFonts.TryMatchCharacter( + codePoint, + FontStyle.Regular, + "Segoe UI", + CultureInfo.GetCultureInfo("en-US"), + out FontMatch match)); + + Font font = match.Family.CreateFont(12F, match.Style); + + Assert.True(font.TryGetGlyphId(codePoint, out ushort glyphId)); + Assert.NotEqual(0, glyphId); + } + + [Fact] + public void SystemFonts_TryMatchCharacter_MatchesBasicLatinOnSupportedPlatforms() + { + if (!TestEnvironment.IsWindows && !TestEnvironment.IsLinux && !TestEnvironment.IsMacOS) + { + return; + } + + CodePoint codePoint = new('A'); + + Assert.True(SystemFonts.TryMatchCharacter( + codePoint, + FontStyle.Regular, + familyName: null, + CultureInfo.GetCultureInfo("en-US"), + out FontMatch match)); + + Font font = match.Family.CreateFont(12F, match.Style); + + Assert.True(font.TryGetGlyphId(codePoint, out ushort glyphId)); + Assert.NotEqual(0, glyphId); + } + [Fact] public void CanEnumerateSystemFontMetrics() { @@ -130,7 +208,6 @@ public void CanGetAllStylesByCulture() } [Fact] - [AppContextSwitch("Switch.SixLabors.Fonts.DoNotUseNativeSystemFontsEnumeration", true)] public void SystemFonts_FontFamilyNotFound_ThrowsWithSearchDirectories() { static void Action() => new SystemFontCollection().Get("AFontThatDoesNotExist"); @@ -159,17 +236,4 @@ public void SystemFonts_FontFamilyNotFound_ThrowsWithSearchDirectories() Assert.Contains(exception.SearchDirectories, e => e.Contains("/system/fonts/", StringComparison.OrdinalIgnoreCase)); } } - - [Fact] - public void SystemFonts_FontFamilyNotFound_ThrowsWithoutSearchDirectories() - { - static void Action() => new SystemFontCollection().Get("AFontThatDoesNotExist"); - - FontFamilyNotFoundException exception = Assert.Throws(Action); - if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - { - Assert.DoesNotContain("/Library/Fonts/", exception.Message); - Assert.Empty(exception.SearchDirectories); - } - } } diff --git a/tests/SixLabors.Fonts.Tests/Tables/AdvancedTypographic/GPos/GPosTableTests.cs b/tests/SixLabors.Fonts.Tests/Tables/AdvancedTypographic/GPos/GPosTableTests.cs index 84c7a5689..ec7089646 100644 --- a/tests/SixLabors.Fonts.Tests/Tables/AdvancedTypographic/GPos/GPosTableTests.cs +++ b/tests/SixLabors.Fonts.Tests/Tables/AdvancedTypographic/GPos/GPosTableTests.cs @@ -26,7 +26,7 @@ public void SingleAdjustmentPositioning_Format1_Works() ]; // act - TextRenderer.RenderTextTo(renderer, testStr, new TextOptions(font)); + TextRenderer.RenderTo(renderer, testStr, new TextOptions(font)); // assert Assert.Equal(expectedGlyphIndices.Length, renderer.GlyphKeys.Count); @@ -61,7 +61,7 @@ public void SingleAdjustmentPositioning_Format2_Works() ]; // act - TextRenderer.RenderTextTo(renderer, testStr, new TextOptions(font)); + TextRenderer.RenderTo(renderer, testStr, new TextOptions(font)); // assert Assert.Equal(expectedGlyphIndices.Length, renderer.GlyphKeys.Count); @@ -97,7 +97,7 @@ public void PairAdjustmentPositioning_Format1_Works() ]; // act - TextRenderer.RenderTextTo(renderer, testStr, new TextOptions(font)); + TextRenderer.RenderTo(renderer, testStr, new TextOptions(font)); // assert Assert.Equal(expectedGlyphIndices.Length, renderer.GlyphKeys.Count); @@ -136,7 +136,7 @@ public void CursiveAttachmentPositioning_Format1_Works() ]; // act - TextRenderer.RenderTextTo(renderer, testStr, new TextOptions(font)); + TextRenderer.RenderTo(renderer, testStr, new TextOptions(font)); // assert Assert.Equal(expectedGlyphIndices.Length, renderer.GlyphKeys.Count); @@ -171,7 +171,7 @@ public void MarkToBaseAttachmentPositioning_Format1_Works() ]; // act - TextRenderer.RenderTextTo(renderer, testStr, new TextOptions(font)); + TextRenderer.RenderTo(renderer, testStr, new TextOptions(font)); // assert Assert.Equal(expectedGlyphIndices.Length, renderer.GlyphKeys.Count); @@ -206,7 +206,7 @@ public void MarkToLigatureAttachmentPositioning_Format1_Works() ]; // act - TextRenderer.RenderTextTo(renderer, testStr, new TextOptions(font)); + TextRenderer.RenderTo(renderer, testStr, new TextOptions(font)); // assert Assert.Equal(expectedGlyphIndices.Length, renderer.GlyphKeys.Count); @@ -241,7 +241,7 @@ public void MarkToMarkAttachmentPositioning_Format1_Works() ]; // act - TextRenderer.RenderTextTo(renderer, testStr, new TextOptions(font)); + TextRenderer.RenderTo(renderer, testStr, new TextOptions(font)); // assert Assert.Equal(expectedGlyphIndices.Length, renderer.GlyphKeys.Count); @@ -277,7 +277,7 @@ public void ContextualPositioning_Format1_Works() ]; // act - TextRenderer.RenderTextTo(renderer, testStr, new TextOptions(font)); + TextRenderer.RenderTo(renderer, testStr, new TextOptions(font)); // assert Assert.Equal(expectedGlyphIndices.Length, renderer.GlyphKeys.Count); @@ -313,7 +313,7 @@ public void ContextualPositioning_Format2_Works() ]; // act - TextRenderer.RenderTextTo(renderer, testStr, new TextOptions(font)); + TextRenderer.RenderTo(renderer, testStr, new TextOptions(font)); // assert Assert.Equal(expectedGlyphIndices.Length, renderer.GlyphKeys.Count); @@ -350,7 +350,7 @@ public void ContextualPositioning_Format3_Works() ]; // act - TextRenderer.RenderTextTo(renderer, testStr, new TextOptions(font)); + TextRenderer.RenderTo(renderer, testStr, new TextOptions(font)); // assert Assert.Equal(expectedGlyphIndices.Length, renderer.GlyphKeys.Count); @@ -389,7 +389,7 @@ public void ChainedContextsPositioning_Format1_Works() ]; // act - TextRenderer.RenderTextTo(renderer, testStr, new TextOptions(font)); + TextRenderer.RenderTo(renderer, testStr, new TextOptions(font)); // assert Assert.Equal(expectedGlyphIndices.Length, renderer.GlyphKeys.Count); @@ -428,7 +428,7 @@ public void ChainedContextsPositioning_Format2_Works() ]; // act - TextRenderer.RenderTextTo(renderer, testStr, new TextOptions(font)); + TextRenderer.RenderTo(renderer, testStr, new TextOptions(font)); // assert Assert.Equal(expectedGlyphIndices.Length, renderer.GlyphKeys.Count); @@ -467,7 +467,7 @@ public void ChainedContextsPositioning_Format3_Works() ]; // act - TextRenderer.RenderTextTo(renderer, testStr, new TextOptions(font)); + TextRenderer.RenderTo(renderer, testStr, new TextOptions(font)); // assert Assert.Equal(expectedGlyphIndices.Length, renderer.GlyphKeys.Count); @@ -501,7 +501,7 @@ public void MarkAnchoring_Works() ]; // act - TextRenderer.RenderTextTo(renderer, testStr, new TextOptions(font)); + TextRenderer.RenderTo(renderer, testStr, new TextOptions(font)); // assert Assert.Equal(expectedGlyphIndices.Length, renderer.GlyphKeys.Count); @@ -537,7 +537,7 @@ public void MarkToMarkAttachment_Works() ]; // act - TextRenderer.RenderTextTo(renderer, testStr, new TextOptions(font)); + TextRenderer.RenderTo(renderer, testStr, new TextOptions(font)); // assert Assert.Equal(expectedGlyphIndices.Length, renderer.GlyphKeys.Count); 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 9868e34ae..76e2c0f77 100644 --- a/tests/SixLabors.Fonts.Tests/Tables/AdvancedTypographic/GSub/GSubTableTests.Indic.cs +++ b/tests/SixLabors.Fonts.Tests/Tables/AdvancedTypographic/GSub/GSubTableTests.Indic.cs @@ -129,7 +129,7 @@ public void CanShapeKannadaText(KannadaFont font, string input, int[] expectedGl ColorGlyphRenderer renderer = new(); TextOptions options = new(font == KannadaFont.Serif ? KannadaNotoSerifTTF : KannadaNotoSansTTF); // TextLayoutTestUtilities.TestLayout(input, options); - TextRenderer.RenderTextTo(renderer, input, options); + TextRenderer.RenderTo(renderer, input, options); Assert.Equal(expectedGlyphIndices.Length, renderer.GlyphKeys.Count); for (int i = 0; i < expectedGlyphIndices.Length; i++) @@ -154,7 +154,7 @@ public void CanShapeKannadaText(KannadaFont font, string input, int[] expectedGl public void CanShapeTeluguText(string input, int[] expectedGlyphIndices) { ColorGlyphRenderer renderer = new(); - TextRenderer.RenderTextTo(renderer, input, new TextOptions(TeluguNotoSansTTF)); + TextRenderer.RenderTo(renderer, input, new TextOptions(TeluguNotoSansTTF)); Assert.Equal(expectedGlyphIndices.Length, renderer.GlyphKeys.Count); for (int i = 0; i < expectedGlyphIndices.Length; i++) @@ -203,7 +203,7 @@ public void CanShapeTeluguText(string input, int[] expectedGlyphIndices) public void CanShapeTamilText(string input, int[] expectedGlyphIndices) { ColorGlyphRenderer renderer = new(); - TextRenderer.RenderTextTo(renderer, input, new TextOptions(TamilNotoSansTTF)); + TextRenderer.RenderTo(renderer, input, new TextOptions(TamilNotoSansTTF)); Assert.Equal(expectedGlyphIndices.Length, renderer.GlyphKeys.Count); for (int i = 0; i < expectedGlyphIndices.Length; i++) @@ -258,7 +258,7 @@ public void CanShapeTamilText(string input, int[] expectedGlyphIndices) public void CanShapeDevanagariTextWithJoiners(string input, int[] expectedGlyphIndices) { ColorGlyphRenderer renderer = new(); - TextRenderer.RenderTextTo(renderer, input, new TextOptions(DevanagariNotoSansTTF)); + TextRenderer.RenderTo(renderer, input, new TextOptions(DevanagariNotoSansTTF)); Assert.Equal(expectedGlyphIndices.Length, renderer.GlyphKeys.Count); for (int i = 0; i < expectedGlyphIndices.Length; i++) @@ -315,7 +315,7 @@ public void CanShapeDevanagariTextWithJoiners(string input, int[] expectedGlyphI public void CanShapeDevanagariText(string input, int[] expectedGlyphIndices) { ColorGlyphRenderer renderer = new(); - TextRenderer.RenderTextTo(renderer, input, new TextOptions(DevanagariNotoSansTTF)); + TextRenderer.RenderTo(renderer, input, new TextOptions(DevanagariNotoSansTTF)); Assert.Equal(expectedGlyphIndices.Length, renderer.GlyphKeys.Count); for (int i = 0; i < expectedGlyphIndices.Length; i++) @@ -336,7 +336,7 @@ public void CanShapeDevanagariText(string input, int[] expectedGlyphIndices) public void CanShapeDevanagariTextWithDottedCircle(string input, int[] expectedGlyphIndices) { ColorGlyphRenderer renderer = new(); - TextRenderer.RenderTextTo(renderer, input, new TextOptions(DevanagariNotoSansTTF)); + TextRenderer.RenderTo(renderer, input, new TextOptions(DevanagariNotoSansTTF)); Assert.Equal(expectedGlyphIndices.Length, renderer.GlyphKeys.Count); for (int i = 0; i < expectedGlyphIndices.Length; i++) @@ -356,7 +356,7 @@ public void CanShapeDevanagariTextWithDottedCircle(string input, int[] expectedG public void CanShapeDevanagariTextWithEyelash(string input, int[] expectedGlyphIndices) { ColorGlyphRenderer renderer = new(); - TextRenderer.RenderTextTo(renderer, input, new TextOptions(DevanagariNotoSansTTF)); + TextRenderer.RenderTo(renderer, input, new TextOptions(DevanagariNotoSansTTF)); Assert.Equal(expectedGlyphIndices.Length, renderer.GlyphKeys.Count); for (int i = 0; i < expectedGlyphIndices.Length; i++) @@ -426,7 +426,7 @@ public void CanShapeDevanagariTextWithEyelash(string input, int[] expectedGlyphI public void CanShapeBengaliText(string input, int[] expectedGlyphIndices) { ColorGlyphRenderer renderer = new(); - TextRenderer.RenderTextTo(renderer, input, new TextOptions(BengaliNotoSansTTF)); + TextRenderer.RenderTo(renderer, input, new TextOptions(BengaliNotoSansTTF)); Assert.Equal(expectedGlyphIndices.Length, renderer.GlyphKeys.Count); for (int i = 0; i < expectedGlyphIndices.Length; i++) @@ -441,7 +441,7 @@ public void CanShapeBengaliText(string input, int[] expectedGlyphIndices) public void CanShapeGurmukhiText(string input, int[] expectedGlyphIndices) { ColorGlyphRenderer renderer = new(); - TextRenderer.RenderTextTo(renderer, input, new TextOptions(GurmukhiNotoSansTTF)); + TextRenderer.RenderTo(renderer, input, new TextOptions(GurmukhiNotoSansTTF)); Assert.Equal(expectedGlyphIndices.Length, renderer.GlyphKeys.Count); for (int i = 0; i < expectedGlyphIndices.Length; i++) @@ -479,7 +479,7 @@ public void CanShapeGurmukhiText_WithConjuncts() ]; ColorGlyphRenderer renderer = new(); - TextRenderer.RenderTextTo(renderer, input, new TextOptions(GurmukhiNotoSansTTF)); + TextRenderer.RenderTo(renderer, input, new TextOptions(GurmukhiNotoSansTTF)); Assert.Equal(expectedGlyphIndices.Length, renderer.GlyphKeys.Count); for (int i = 0; i < expectedGlyphIndices.Length; i++) @@ -530,7 +530,7 @@ public void IndicShapingCategoryIsCorrect(int codePoint, int expectedCategory) public void CanShapeGujaratiText(string input, int[] expectedGlyphIndices) { ColorGlyphRenderer renderer = new(); - TextRenderer.RenderTextTo(renderer, input, new TextOptions(GujaratiNotoSansTTF)); + TextRenderer.RenderTo(renderer, input, new TextOptions(GujaratiNotoSansTTF)); Assert.Equal(expectedGlyphIndices.Length, renderer.GlyphKeys.Count); for (int i = 0; i < expectedGlyphIndices.Length; i++) @@ -614,7 +614,7 @@ public void CanShapeGujaratiText(string input, int[] expectedGlyphIndices) public void CanShapeMalayalamText(string input, int[] expectedGlyphIndices) { ColorGlyphRenderer renderer = new(); - TextRenderer.RenderTextTo(renderer, input, new TextOptions(MalayalamNotoSansTTF)); + TextRenderer.RenderTo(renderer, input, new TextOptions(MalayalamNotoSansTTF)); Assert.Equal(expectedGlyphIndices.Length, renderer.GlyphKeys.Count); for (int i = 0; i < expectedGlyphIndices.Length; i++) @@ -635,7 +635,7 @@ public void CanShapeMalayalamText(string input, int[] expectedGlyphIndices) public void CanShapeOriyaText(string input, int[] expectedGlyphIndices) { ColorGlyphRenderer renderer = new(); - TextRenderer.RenderTextTo(renderer, input, new TextOptions(OriyaNotoSansTTF)); + TextRenderer.RenderTo(renderer, input, new TextOptions(OriyaNotoSansTTF)); Assert.Equal(expectedGlyphIndices.Length, renderer.GlyphKeys.Count); for (int i = 0; i < expectedGlyphIndices.Length; i++) @@ -675,7 +675,7 @@ public void CanShapeOriyaText(string input, int[] expectedGlyphIndices) public void CanShapeKhmerText(string input, int[] expectedGlyphIndices) { ColorGlyphRenderer renderer = new(); - TextRenderer.RenderTextTo(renderer, input, new TextOptions(KhmerNotoSansTTF)); + TextRenderer.RenderTo(renderer, input, new TextOptions(KhmerNotoSansTTF)); Assert.Equal(expectedGlyphIndices.Length, renderer.GlyphKeys.Count); for (int i = 0; i < expectedGlyphIndices.Length; i++) 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 4f51697ae..95a99f8bb 100644 --- a/tests/SixLabors.Fonts.Tests/Tables/AdvancedTypographic/Gsub/GSubTableTests.Hangul.cs +++ b/tests/SixLabors.Fonts.Tests/Tables/AdvancedTypographic/Gsub/GSubTableTests.Hangul.cs @@ -24,7 +24,7 @@ public void ShouldUseComposedSyllablesCFF() int[] expectedGlyphIndices = [21324, 10264, 1, 10264, 14, 14, 1, 9, 16956, 14, 14, 10]; // act - TextRenderer.RenderTextTo(renderer, input, new TextOptions(this.hangulFontCFF)); + TextRenderer.RenderTo(renderer, input, new TextOptions(this.hangulFontCFF)); // assert Assert.Equal(expectedGlyphIndices.Length, renderer.GlyphKeys.Count); @@ -43,7 +43,7 @@ public void ShouldComposeDecomposedSyllablesCFF() int[] expectedGlyphIndices = [21324, 10264, 1, 10264, 14, 14, 1, 9, 16956, 14, 14, 10]; // act - TextRenderer.RenderTextTo(renderer, input, new TextOptions(this.hangulFontCFF)); + TextRenderer.RenderTo(renderer, input, new TextOptions(this.hangulFontCFF)); // assert Assert.Equal(expectedGlyphIndices.Length, renderer.GlyphKeys.Count); @@ -62,7 +62,7 @@ public void ShouldUseOTFeaturesForNonCombining_L_V_T_CFF() int[] expectedGlyphIndices = [23511, 23860, 24202]; // act - TextRenderer.RenderTextTo(renderer, input, new TextOptions(this.hangulFontCFF)); + TextRenderer.RenderTo(renderer, input, new TextOptions(this.hangulFontCFF)); // assert Assert.Equal(expectedGlyphIndices.Length, renderer.GlyphKeys.Count); @@ -84,7 +84,7 @@ public void ShouldDecompose_LV_T_To_L_V_T_If_LVT_IsNotSupportedCFF() int[] expectedGlyphIndices = [23168, 23789, 24065]; // act - TextRenderer.RenderTextTo(renderer, input, new TextOptions(this.hangulFontCFF)); + TextRenderer.RenderTo(renderer, input, new TextOptions(this.hangulFontCFF)); // assert Assert.Equal(expectedGlyphIndices.Length, renderer.GlyphKeys.Count); @@ -103,7 +103,7 @@ public void ShouldReorderToneMarksToBeginningOf_L_V_SyllablesCFF() int[] expectedGlyphIndices = [1443, 23759, 23954]; // act - TextRenderer.RenderTextTo(renderer, input, new TextOptions(this.hangulFontCFF)); + TextRenderer.RenderTo(renderer, input, new TextOptions(this.hangulFontCFF)); // assert Assert.Equal(expectedGlyphIndices.Length, renderer.GlyphKeys.Count); @@ -125,7 +125,7 @@ public void ShouldUseComposedSyllablesTTF() int[] expectedGlyphIndices = [2953, 636, 3, 636, 16, 16, 3, 11, 2077, 16, 16, 12]; // act - TextRenderer.RenderTextTo(renderer, input, new TextOptions(this.hangulFontTTF)); + TextRenderer.RenderTo(renderer, input, new TextOptions(this.hangulFontTTF)); // assert Assert.Equal(expectedGlyphIndices.Length, renderer.GlyphKeys.Count); @@ -144,7 +144,7 @@ public void ShouldComposeDecomposedSyllablesTTF() int[] expectedGlyphIndices = [2953, 636, 3, 636, 16, 16, 3, 11, 2077, 16, 16, 12]; // act - TextRenderer.RenderTextTo(renderer, input, new TextOptions(this.hangulFontTTF)); + TextRenderer.RenderTo(renderer, input, new TextOptions(this.hangulFontTTF)); // assert Assert.Equal(expectedGlyphIndices.Length, renderer.GlyphKeys.Count); @@ -163,7 +163,7 @@ public void ShouldUseOTFeaturesForNonCombining_L_V_T_TTF() int[] expectedGlyphIndices = [21150, 21436, 21569]; // act - TextRenderer.RenderTextTo(renderer, input, new TextOptions(this.hangulFontTTF)); + TextRenderer.RenderTo(renderer, input, new TextOptions(this.hangulFontTTF)); // assert Assert.Equal(expectedGlyphIndices.Length, renderer.GlyphKeys.Count); @@ -185,7 +185,7 @@ public void ShouldDecompose_LV_T_To_L_V_T_If_LVT_IsNotSupportedTTF() int[] expectedGlyphIndices = [20667, 21294, 21569]; // act - TextRenderer.RenderTextTo(renderer, input, new TextOptions(this.hangulFontTTF)); + TextRenderer.RenderTo(renderer, input, new TextOptions(this.hangulFontTTF)); // assert Assert.Equal(expectedGlyphIndices.Length, renderer.GlyphKeys.Count); @@ -204,7 +204,7 @@ public void ShouldReorderToneMarksToBeginningOf_L_V_SyllablesTTF() int[] expectedGlyphIndices = [20665, 21150, 21435]; // act - TextRenderer.RenderTextTo(renderer, input, new TextOptions(this.hangulFontTTF)); + TextRenderer.RenderTo(renderer, input, new TextOptions(this.hangulFontTTF)); // assert Assert.Equal(expectedGlyphIndices.Length, renderer.GlyphKeys.Count); @@ -223,7 +223,7 @@ public void ShouldReorderToneMarksToBeginningOf_L_V_T_Syllables() int[] expectedGlyphIndices = [20665, 21150, 21436, 21569]; // act - TextRenderer.RenderTextTo(renderer, input, new TextOptions(this.hangulFontTTF)); + TextRenderer.RenderTo(renderer, input, new TextOptions(this.hangulFontTTF)); // assert Assert.Equal(expectedGlyphIndices.Length, renderer.GlyphKeys.Count); @@ -242,7 +242,7 @@ public void ShouldReorderToneMarksToBeginningOf_LV_Syllables() int[] expectedGlyphIndices = [20665, 636]; // act - TextRenderer.RenderTextTo(renderer, input, new TextOptions(this.hangulFontTTF)); + TextRenderer.RenderTo(renderer, input, new TextOptions(this.hangulFontTTF)); // assert Assert.Equal(expectedGlyphIndices.Length, renderer.GlyphKeys.Count); @@ -261,7 +261,7 @@ public void ShouldReorderToneMarksToBeginningOf_LVT_Syllables() int[] expectedGlyphIndices = [20665, 637]; // act - TextRenderer.RenderTextTo(renderer, input, new TextOptions(this.hangulFontTTF)); + TextRenderer.RenderTo(renderer, input, new TextOptions(this.hangulFontTTF)); // assert Assert.Equal(expectedGlyphIndices.Length, renderer.GlyphKeys.Count); @@ -280,7 +280,7 @@ public void ShouldInsertDottedCircleForInvalidToneMarks() int[] expectedGlyphIndices = [2986, 20665, 21620, 3078]; // act - TextRenderer.RenderTextTo(renderer, input, new TextOptions(this.hangulFontTTF)); + TextRenderer.RenderTo(renderer, input, new TextOptions(this.hangulFontTTF)); // assert Assert.Equal(expectedGlyphIndices.Length, renderer.GlyphKeys.Count); 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 8fc74efc0..baa0acd30 100644 --- a/tests/SixLabors.Fonts.Tests/Tables/AdvancedTypographic/Gsub/GSubTableTests.Universal.cs +++ b/tests/SixLabors.Fonts.Tests/Tables/AdvancedTypographic/Gsub/GSubTableTests.Universal.cs @@ -53,7 +53,7 @@ public partial class GSubTableTests public void CanShapeBalineseText(string input, int[] expectedGlyphIndices) { ColorGlyphRenderer renderer = new(); - TextRenderer.RenderTextTo(renderer, input, new TextOptions(BalineseFontTTF)); + TextRenderer.RenderTo(renderer, input, new TextOptions(BalineseFontTTF)); Assert.Equal(expectedGlyphIndices.Length, renderer.GlyphKeys.Count); for (int i = 0; i < expectedGlyphIndices.Length; i++) diff --git a/tests/SixLabors.Fonts.Tests/Tables/AdvancedTypographic/Gsub/GSubTableTests.cs b/tests/SixLabors.Fonts.Tests/Tables/AdvancedTypographic/Gsub/GSubTableTests.cs index 70f06279f..6d8063b2f 100644 --- a/tests/SixLabors.Fonts.Tests/Tables/AdvancedTypographic/Gsub/GSubTableTests.cs +++ b/tests/SixLabors.Fonts.Tests/Tables/AdvancedTypographic/Gsub/GSubTableTests.cs @@ -45,7 +45,7 @@ public void RenderArabicCharacters_WithIsolatedForm_Works(string testStr, int ex ColorGlyphRenderer renderer = new(); // act - TextRenderer.RenderTextTo(renderer, testStr, new TextOptions(arabicFont)); + TextRenderer.RenderTo(renderer, testStr, new TextOptions(arabicFont)); // assert GlyphRendererParameters glyphKey = Assert.Single(renderer.GlyphKeys); @@ -64,7 +64,7 @@ public void SingleSubstitution_Works() int expectedGlyphIndex = 38; // we expect A to be mapped to B. // act - TextRenderer.RenderTextTo(renderer, testStr, new TextOptions(font)); + TextRenderer.RenderTo(renderer, testStr, new TextOptions(font)); // assert GlyphRendererParameters glyphKey = Assert.Single(renderer.GlyphKeys); @@ -81,7 +81,7 @@ public void ContextualFractions_WithFractionSlash_Works() int[] expectedGlyphIndices = [580, 404, 453]; // act - TextRenderer.RenderTextTo(renderer, testStr, new TextOptions(font) { FeatureTags = new Tag[] { KnownFeatureTags.Numerators, KnownFeatureTags.Denominators } }); + TextRenderer.RenderTo(renderer, testStr, new TextOptions(font) { FeatureTags = new Tag[] { KnownFeatureTags.Numerators, KnownFeatureTags.Denominators } }); // assert Assert.Equal(expectedGlyphIndices.Length, renderer.GlyphKeys.Count); @@ -101,7 +101,7 @@ public void ContextualFractions_WithSlash_Works() int[] expectedGlyphIndices = [580, 404, 453]; // act - TextRenderer.RenderTextTo(renderer, testStr, new TextOptions(font) { FeatureTags = new Tag[] { KnownFeatureTags.Fractions } }); + TextRenderer.RenderTo(renderer, testStr, new TextOptions(font) { FeatureTags = new Tag[] { KnownFeatureTags.Fractions } }); // assert Assert.Equal(expectedGlyphIndices.Length, renderer.GlyphKeys.Count); @@ -123,7 +123,7 @@ public void MultipleSubstitution_Works() int expectedGlyphIndex = 40; // we expect C to be mapped to D. // act - TextRenderer.RenderTextTo(renderer, testStr, new TextOptions(font)); + TextRenderer.RenderTo(renderer, testStr, new TextOptions(font)); // assert GlyphRendererParameters glyphKey = Assert.Single(renderer.GlyphKeys); @@ -142,7 +142,7 @@ public void AlternateSubstitution_Works() int expectedGlyphIndex = 42; // we expect E to be mapped to F. // act - TextRenderer.RenderTextTo(renderer, testStr, new TextOptions(font)); + TextRenderer.RenderTo(renderer, testStr, new TextOptions(font)); // assert GlyphRendererParameters glyphKey = Assert.Single(renderer.GlyphKeys); @@ -161,7 +161,7 @@ public void LigatureSubstitution_Works() int expectedGlyphIndex = 229; // act - TextRenderer.RenderTextTo(renderer, testStr, new TextOptions(font)); + TextRenderer.RenderTo(renderer, testStr, new TextOptions(font)); // assert GlyphRendererParameters glyphKey = Assert.Single(renderer.GlyphKeys); @@ -180,7 +180,7 @@ public void ContextualSubstitution_Format1_Works() int[] expectedGlyphIndices = [3, 7]; // act - TextRenderer.RenderTextTo(renderer, testStr, new TextOptions(font)); + TextRenderer.RenderTo(renderer, testStr, new TextOptions(font)); // assert Assert.Equal(expectedGlyphIndices.Length, renderer.GlyphKeys.Count); @@ -202,7 +202,7 @@ public void ContextualSubstitution_Format2_Works() int[] expectedGlyphIndices = [3, 7]; // act - TextRenderer.RenderTextTo(renderer, testStr, new TextOptions(font)); + TextRenderer.RenderTo(renderer, testStr, new TextOptions(font)); // assert Assert.Equal(expectedGlyphIndices.Length, renderer.GlyphKeys.Count); @@ -224,7 +224,7 @@ public void ContextualSubstitution_Format3_Works() int[] expectedGlyphIndices = [67, 78, 80]; // act - TextRenderer.RenderTextTo(renderer, testStr, new TextOptions(font)); + TextRenderer.RenderTo(renderer, testStr, new TextOptions(font)); // assert Assert.Equal(expectedGlyphIndices.Length, renderer.GlyphKeys.Count); @@ -246,7 +246,7 @@ public void ChainedContextsSubstitution_Format1_Works() int[] expectedGlyphIndices = [22, 63, 64, 25]; // act - TextRenderer.RenderTextTo(renderer, testStr, new TextOptions(font)); + TextRenderer.RenderTo(renderer, testStr, new TextOptions(font)); // assert Assert.Equal(expectedGlyphIndices.Length, renderer.GlyphKeys.Count); @@ -268,7 +268,7 @@ public void ChainedContextsSubstitution_Format2_Works() int[] expectedGlyphIndices = [22, 23, 64, 25]; // act - TextRenderer.RenderTextTo(renderer, testStr, new TextOptions(font)); + TextRenderer.RenderTo(renderer, testStr, new TextOptions(font)); // assert Assert.Equal(expectedGlyphIndices.Length, renderer.GlyphKeys.Count); @@ -290,7 +290,7 @@ public void ChainedContextsSubstitution_Format3_Works() int[] expectedGlyphIndices = [89, 31, 90]; // act - TextRenderer.RenderTextTo(renderer, testStr, new TextOptions(font)); + TextRenderer.RenderTo(renderer, testStr, new TextOptions(font)); // assert Assert.Equal(expectedGlyphIndices.Length, renderer.GlyphKeys.Count); @@ -312,7 +312,7 @@ public void ChainedContextsSubstitution_Format3_WithCursiveScript_Works() int[] expectedGlyphIndices = [69, 102]; // act - TextRenderer.RenderTextTo(renderer, testStr, new TextOptions(font)); + TextRenderer.RenderTo(renderer, testStr, new TextOptions(font)); // assert Assert.Equal(expectedGlyphIndices.Length, renderer.GlyphKeys.Count); @@ -334,7 +334,7 @@ public void ReverseChainingContextualSingleSubstitution_Works() int[] expectedGlyphIndices = [57, 58, 59]; // act - TextRenderer.RenderTextTo(renderer, testStr, new TextOptions(font)); + TextRenderer.RenderTo(renderer, testStr, new TextOptions(font)); // assert Assert.Equal(expectedGlyphIndices.Length, renderer.GlyphKeys.Count); @@ -355,7 +355,7 @@ public void OldStyleFiguresFeature_Works() int[] expectedGlyphIndices = [2242, 2243, 2244, 2245, 2246, 2247]; // act - TextRenderer.RenderTextTo(renderer, testStr, new TextOptions(font) + TextRenderer.RenderTo(renderer, testStr, new TextOptions(font) { FeatureTags = new List { KnownFeatureTags.OldstyleFigures } }); diff --git a/tests/SixLabors.Fonts.Tests/Tables/Variations/VariationFontTests.cs b/tests/SixLabors.Fonts.Tests/Tables/Variations/VariationFontTests.cs index 5e1536c8a..f2833d3dc 100644 --- a/tests/SixLabors.Fonts.Tests/Tables/Variations/VariationFontTests.cs +++ b/tests/SixLabors.Fonts.Tests/Tables/Variations/VariationFontTests.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using SixLabors.Fonts.Rendering; +using SixLabors.Fonts.Tables.AdvancedTypographic; using SixLabors.Fonts.Tables.AdvancedTypographic.Variations; using SixLabors.Fonts.Unicode; @@ -17,7 +18,7 @@ public class VariationFontTests public void FontVariation_ValidTag_CreatesInstance() { FontVariation variation = new("wght", 700); - Assert.Equal("wght", variation.Tag); + Assert.Equal(KnownVariationAxes.Weight, variation.Tag); Assert.Equal(700, variation.Value); } @@ -41,7 +42,7 @@ public void CanCreateFontWithVariations() Font variedFont = new(baseFont, new FontVariation("wght", 700)); Assert.Single(variedFont.Variations.ToArray()); - Assert.Equal("wght", variedFont.Variations[0].Tag); + Assert.Equal(KnownVariationAxes.Weight, variedFont.Variations[0].Tag); Assert.Equal(700, variedFont.Variations[0].Value); } @@ -84,7 +85,7 @@ public void CanLoadVariationAxes_RobotoFlex() Assert.True(font.FontMetrics.TryGetVariationAxes(out ReadOnlyMemory axes)); Assert.Equal(13, axes.Length); - Assert.Equal("wght", axes.Span[0].Tag); + Assert.Equal(KnownVariationAxes.Weight, axes.Span[0].Tag); Assert.Equal(100, axes.Span[0].Min); Assert.Equal(1000, axes.Span[0].Max); Assert.Equal(400, axes.Span[0].Default); @@ -99,11 +100,11 @@ public void CanLoadVariationAxes_AdobeVFPrototype() Assert.True(font.FontMetrics.TryGetVariationAxes(out ReadOnlyMemory axes)); Assert.Equal(2, axes.Length); - Assert.Equal("wght", axes.Span[0].Tag); + Assert.Equal(KnownVariationAxes.Weight, axes.Span[0].Tag); Assert.Equal(200, axes.Span[0].Min); Assert.Equal(900, axes.Span[0].Max); - Assert.Equal("CNTR", axes.Span[1].Tag); + Assert.Equal(Tag.Parse("CNTR"), axes.Span[1].Tag); } [Fact] @@ -208,7 +209,7 @@ public void HVAR_DefaultWeightPreservesOriginalWidth() Font defaultFont = family.CreateFont(12); Assert.True(defaultFont.FontMetrics.TryGetVariationAxes(out ReadOnlyMemory axes)); - VariationAxis wghtAxis = Assert.Single(axes.ToArray(), a => a.Tag == "wght"); + VariationAxis wghtAxis = Assert.Single(axes.ToArray(), a => a.Tag == KnownVariationAxes.Weight); Font variedFont = family.CreateFont(12, new FontVariation("wght", wghtAxis.Default)); @@ -275,10 +276,10 @@ public void GVar_AdobeVFPrototype_GSUB_SubstitutesGlyphAtHeavyWeight() Font heavyFont = family.CreateFont(12, new FontVariation("wght", 900)); GlyphRenderer defaultRenderer = new(); - TextRenderer.RenderTextTo(defaultRenderer, "$", new TextOptions(defaultFont)); + TextRenderer.RenderTo(defaultRenderer, "$", new TextOptions(defaultFont)); GlyphRenderer heavyRenderer = new(); - TextRenderer.RenderTextTo(heavyRenderer, "$", new TextOptions(heavyFont)); + TextRenderer.RenderTo(heavyRenderer, "$", new TextOptions(heavyFont)); // The GSUB substitution should produce a different glyph ID at wght=900. Assert.NotEqual(defaultRenderer.GlyphKeys[0].GlyphId, heavyRenderer.GlyphKeys[0].GlyphId); @@ -303,7 +304,7 @@ public void CFF2_CanLoadVariationAxes() Assert.True(font.FontMetrics.TryGetVariationAxes(out ReadOnlyMemory axes)); Assert.Equal(2, axes.Length); - Assert.Equal("wght", axes.Span[0].Tag); + Assert.Equal(KnownVariationAxes.Weight, axes.Span[0].Tag); } [Fact] @@ -322,7 +323,7 @@ public void CFF2_RendersGlyphAtDefaultWeight() Font font = family.CreateFont(48); GlyphRenderer renderer = new(); - TextRenderer.RenderTextTo(renderer, "$", new TextOptions(font)); + TextRenderer.RenderTo(renderer, "$", new TextOptions(font)); Assert.NotEmpty(renderer.GlyphKeys); Assert.NotEmpty(renderer.ControlPoints); @@ -335,7 +336,7 @@ public void CFF2_RendersGlyphAtVariedWeight() Font font = family.CreateFont(48, new FontVariation("wght", 900)); GlyphRenderer renderer = new(); - TextRenderer.RenderTextTo(renderer, "$", new TextOptions(font)); + TextRenderer.RenderTo(renderer, "$", new TextOptions(font)); Assert.NotEmpty(renderer.GlyphKeys); Assert.NotEmpty(renderer.ControlPoints); @@ -350,10 +351,10 @@ public void CFF2_MultipleWeightsRenderSuccessfully() Font heavyFont = family.CreateFont(48, new FontVariation("wght", 900)); GlyphRenderer lightRenderer = new(); - TextRenderer.RenderTextTo(lightRenderer, "$", new TextOptions(lightFont)); + TextRenderer.RenderTo(lightRenderer, "$", new TextOptions(lightFont)); GlyphRenderer heavyRenderer = new(); - TextRenderer.RenderTextTo(heavyRenderer, "$", new TextOptions(heavyFont)); + TextRenderer.RenderTo(heavyRenderer, "$", new TextOptions(heavyFont)); Assert.NotEmpty(lightRenderer.ControlPoints); Assert.NotEmpty(heavyRenderer.ControlPoints); @@ -386,10 +387,10 @@ public void GPOS_MarkAnchorPositionsVaryWithWeight() Font heavyFont = family.CreateFont(72, new FontVariation("wght", 900)); GlyphRenderer defaultRenderer = new(); - TextRenderer.RenderTextTo(defaultRenderer, "\u0641", new TextOptions(defaultFont)); + TextRenderer.RenderTo(defaultRenderer, "\u0641", new TextOptions(defaultFont)); GlyphRenderer heavyRenderer = new(); - TextRenderer.RenderTextTo(heavyRenderer, "\u0641", new TextOptions(heavyFont)); + TextRenderer.RenderTo(heavyRenderer, "\u0641", new TextOptions(heavyFont)); // Both should render, and the glyph bounds should differ due to // GPOS mark anchor adjustments varying with weight. @@ -486,7 +487,7 @@ public void Renderer_VariedFontProducesGlyphs() Font variedFont = family.CreateFont(12, new FontVariation("wght", 300)); GlyphRenderer renderer = new(); - TextRenderer.RenderTextTo(renderer, "彌", new TextOptions(variedFont)); + TextRenderer.RenderTo(renderer, "彌", new TextOptions(variedFont)); Assert.NotEmpty(renderer.GlyphKeys); Assert.NotEmpty(renderer.GlyphRects); @@ -500,10 +501,10 @@ public void Renderer_DifferentVariationsProduceDifferentControlPoints() Font variedFont = family.CreateFont(72, new FontVariation("wght", 300)); GlyphRenderer defaultRenderer = new(); - TextRenderer.RenderTextTo(defaultRenderer, "彌", new TextOptions(defaultFont)); + TextRenderer.RenderTo(defaultRenderer, "彌", new TextOptions(defaultFont)); GlyphRenderer variedRenderer = new(); - TextRenderer.RenderTextTo(variedRenderer, "彌", new TextOptions(variedFont)); + TextRenderer.RenderTo(variedRenderer, "彌", new TextOptions(variedFont)); // Both should produce control points, but they should differ. Assert.NotEmpty(defaultRenderer.ControlPoints); @@ -531,7 +532,7 @@ public void Renderer_GVar_AdobeVFPrototype_VariedFontProducesGlyphs() Font variedFont = family.CreateFont(12, new FontVariation("wght", 900)); GlyphRenderer renderer = new(); - TextRenderer.RenderTextTo(renderer, "A", new TextOptions(variedFont)); + TextRenderer.RenderTo(renderer, "A", new TextOptions(variedFont)); Assert.NotEmpty(renderer.GlyphKeys); } @@ -567,10 +568,10 @@ public void NotoEmoji_GVar_OutlinesVaryWithWeight() // Render both and verify outlines differ. GlyphRenderer lightRenderer = new(); - TextRenderer.RenderTextTo(lightRenderer, "\u2B50", new TextOptions(lightFont)); + TextRenderer.RenderTo(lightRenderer, "\u2B50", new TextOptions(lightFont)); GlyphRenderer boldRenderer = new(); - TextRenderer.RenderTextTo(boldRenderer, "\u2B50", new TextOptions(boldFont)); + TextRenderer.RenderTo(boldRenderer, "\u2B50", new TextOptions(boldFont)); Assert.NotEmpty(lightRenderer.ControlPoints); Assert.NotEmpty(boldRenderer.ControlPoints); @@ -668,9 +669,9 @@ public void CVar_CanLoadFontWithCvarTable() Assert.NotNull(font.FontMetrics); Assert.True(font.FontMetrics.TryGetVariationAxes(out ReadOnlyMemory axes)); Assert.Equal(3, axes.Length); - Assert.Equal("wght", axes.Span[0].Tag); - Assert.Equal("wdth", axes.Span[1].Tag); - Assert.Equal("opsz", axes.Span[2].Tag); + Assert.Equal(KnownVariationAxes.Weight, axes.Span[0].Tag); + Assert.Equal(KnownVariationAxes.Width, axes.Span[1].Tag); + Assert.Equal(KnownVariationAxes.OpticalSize, axes.Span[2].Tag); } [Fact] @@ -698,10 +699,10 @@ public void CVar_HintedRenderingWithVariation() TextOptions variedOptions = new(variedFont) { HintingMode = HintingMode.Standard }; GlyphRenderer defaultRenderer = new(); - TextRenderer.RenderTextTo(defaultRenderer, "hono", defaultOptions); + TextRenderer.RenderTo(defaultRenderer, "hono", defaultOptions); GlyphRenderer variedRenderer = new(); - TextRenderer.RenderTextTo(variedRenderer, "hono", variedOptions); + TextRenderer.RenderTo(variedRenderer, "hono", variedOptions); Assert.NotEmpty(defaultRenderer.ControlPoints); Assert.NotEmpty(variedRenderer.ControlPoints); @@ -717,7 +718,7 @@ public void CVar_NoShared_HintedRenderingWithVariation() TextOptions options = new(variedFont) { HintingMode = HintingMode.Standard }; GlyphRenderer renderer = new(); - TextRenderer.RenderTextTo(renderer, "hono", options); + TextRenderer.RenderTo(renderer, "hono", options); Assert.NotEmpty(renderer.ControlPoints); } @@ -734,10 +735,10 @@ public void CVar_HintedRenderingAtSmallSize() TextOptions unhintedOptions = new(font) { HintingMode = HintingMode.None }; GlyphRenderer hintedRenderer = new(); - TextRenderer.RenderTextTo(hintedRenderer, "hono", hintedOptions); + TextRenderer.RenderTo(hintedRenderer, "hono", hintedOptions); GlyphRenderer unhintedRenderer = new(); - TextRenderer.RenderTextTo(unhintedRenderer, "hono", unhintedOptions); + TextRenderer.RenderTo(unhintedRenderer, "hono", unhintedOptions); Assert.NotEmpty(hintedRenderer.ControlPoints); Assert.NotEmpty(unhintedRenderer.ControlPoints); diff --git a/tests/SixLabors.Fonts.Tests/Tables/Variations/VariationsTests.cs b/tests/SixLabors.Fonts.Tests/Tables/Variations/VariationsTests.cs index b68b6c3a2..11a74517d 100644 --- a/tests/SixLabors.Fonts.Tests/Tables/Variations/VariationsTests.cs +++ b/tests/SixLabors.Fonts.Tests/Tables/Variations/VariationsTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.Fonts.Tables.AdvancedTypographic; using SixLabors.Fonts.Tables.AdvancedTypographic.Variations; namespace SixLabors.Fonts.Tests.Tables.Variations; @@ -21,79 +22,79 @@ public void CanLoadVariationTables_RobotoFlex() Assert.Equal(13, variationAxes.Length); Assert.Equal("wght", variationAxes.Span[0].Name); - Assert.Equal("wght", variationAxes.Span[0].Tag); + Assert.Equal(KnownVariationAxes.Weight, variationAxes.Span[0].Tag); Assert.Equal(100, variationAxes.Span[0].Min); Assert.Equal(1000, variationAxes.Span[0].Max); Assert.Equal(400, variationAxes.Span[0].Default); Assert.Equal("wdth", variationAxes.Span[1].Name); - Assert.Equal("wdth", variationAxes.Span[1].Tag); + Assert.Equal(KnownVariationAxes.Width, variationAxes.Span[1].Tag); Assert.Equal(25, variationAxes.Span[1].Min); Assert.Equal(151, variationAxes.Span[1].Max); Assert.Equal(100, variationAxes.Span[1].Default); Assert.Equal("opsz", variationAxes.Span[2].Name); - Assert.Equal("opsz", variationAxes.Span[2].Tag); + Assert.Equal(KnownVariationAxes.OpticalSize, variationAxes.Span[2].Tag); Assert.Equal(8, variationAxes.Span[2].Min); Assert.Equal(144, variationAxes.Span[2].Max); Assert.Equal(14, variationAxes.Span[2].Default); Assert.Equal("GRAD", variationAxes.Span[3].Name); - Assert.Equal("GRAD", variationAxes.Span[3].Tag); + Assert.Equal(Tag.Parse("GRAD"), variationAxes.Span[3].Tag); Assert.Equal(-200, variationAxes.Span[3].Min); Assert.Equal(150, variationAxes.Span[3].Max); Assert.Equal(0, variationAxes.Span[3].Default); Assert.Equal("slnt", variationAxes.Span[4].Name); - Assert.Equal("slnt", variationAxes.Span[4].Tag); + Assert.Equal(KnownVariationAxes.Slant, variationAxes.Span[4].Tag); Assert.Equal(-10, variationAxes.Span[4].Min); Assert.Equal(0, variationAxes.Span[4].Max); Assert.Equal(0, variationAxes.Span[4].Default); Assert.Equal("XTRA", variationAxes.Span[5].Name); - Assert.Equal("XTRA", variationAxes.Span[5].Tag); + Assert.Equal(Tag.Parse("XTRA"), variationAxes.Span[5].Tag); Assert.Equal(323, variationAxes.Span[5].Min); Assert.Equal(603, variationAxes.Span[5].Max); Assert.Equal(468, variationAxes.Span[5].Default); Assert.Equal("XOPQ", variationAxes.Span[6].Name); - Assert.Equal("XOPQ", variationAxes.Span[6].Tag); + Assert.Equal(Tag.Parse("XOPQ"), variationAxes.Span[6].Tag); Assert.Equal(27, variationAxes.Span[6].Min); Assert.Equal(175, variationAxes.Span[6].Max); Assert.Equal(96, variationAxes.Span[6].Default); Assert.Equal("YOPQ", variationAxes.Span[7].Name); - Assert.Equal("YOPQ", variationAxes.Span[7].Tag); + Assert.Equal(Tag.Parse("YOPQ"), variationAxes.Span[7].Tag); Assert.Equal(25, variationAxes.Span[7].Min); Assert.Equal(135, variationAxes.Span[7].Max); Assert.Equal(79, variationAxes.Span[7].Default); Assert.Equal("YTLC", variationAxes.Span[8].Name); - Assert.Equal("YTLC", variationAxes.Span[8].Tag); + Assert.Equal(Tag.Parse("YTLC"), variationAxes.Span[8].Tag); Assert.Equal(416, variationAxes.Span[8].Min); Assert.Equal(570, variationAxes.Span[8].Max); Assert.Equal(514, variationAxes.Span[8].Default); Assert.Equal("YTUC", variationAxes.Span[9].Name); - Assert.Equal("YTUC", variationAxes.Span[9].Tag); + Assert.Equal(Tag.Parse("YTUC"), variationAxes.Span[9].Tag); Assert.Equal(528, variationAxes.Span[9].Min); Assert.Equal(760, variationAxes.Span[9].Max); Assert.Equal(712, variationAxes.Span[9].Default); Assert.Equal("YTAS", variationAxes.Span[10].Name); - Assert.Equal("YTAS", variationAxes.Span[10].Tag); + Assert.Equal(Tag.Parse("YTAS"), variationAxes.Span[10].Tag); Assert.Equal(649, variationAxes.Span[10].Min); Assert.Equal(854, variationAxes.Span[10].Max); Assert.Equal(750, variationAxes.Span[10].Default); Assert.Equal("YTDE", variationAxes.Span[11].Name); - Assert.Equal("YTDE", variationAxes.Span[11].Tag); + Assert.Equal(Tag.Parse("YTDE"), variationAxes.Span[11].Tag); Assert.Equal(-305, variationAxes.Span[11].Min); Assert.Equal(-98, variationAxes.Span[11].Max); Assert.Equal(-203, variationAxes.Span[11].Default); Assert.Equal("YTFI", variationAxes.Span[12].Name); - Assert.Equal("YTFI", variationAxes.Span[12].Tag); + Assert.Equal(Tag.Parse("YTFI"), variationAxes.Span[12].Tag); Assert.Equal(560, variationAxes.Span[12].Min); Assert.Equal(788, variationAxes.Span[12].Max); Assert.Equal(738, variationAxes.Span[12].Default); @@ -106,13 +107,13 @@ public void CanLoadVariationTables_AdobeVFPrototype() Assert.Equal(2, variationAxes.Length); Assert.Equal("Weight", variationAxes.Span[0].Name); - Assert.Equal("wght", variationAxes.Span[0].Tag); + Assert.Equal(KnownVariationAxes.Weight, variationAxes.Span[0].Tag); Assert.Equal(200, variationAxes.Span[0].Min); Assert.Equal(900, variationAxes.Span[0].Max); Assert.Equal(389.344, Math.Round(variationAxes.Span[0].Default, 3)); Assert.Equal("Contrast", variationAxes.Span[1].Name); - Assert.Equal("CNTR", variationAxes.Span[1].Tag); + Assert.Equal(Tag.Parse("CNTR"), variationAxes.Span[1].Tag); Assert.Equal(0, variationAxes.Span[1].Min); Assert.Equal(100, variationAxes.Span[1].Max); Assert.Equal(0, variationAxes.Span[1].Default); diff --git a/tests/SixLabors.Fonts.Tests/TextAlignmentTests.cs b/tests/SixLabors.Fonts.Tests/TextAlignmentTests.cs index 53e0192ce..73ba0b26f 100644 --- a/tests/SixLabors.Fonts.Tests/TextAlignmentTests.cs +++ b/tests/SixLabors.Fonts.Tests/TextAlignmentTests.cs @@ -149,7 +149,7 @@ private static void Draw( }; IReadOnlyList glyphPaths = TextBuilder.GenerateGlyphs(text, textOptions); - TextRenderer.RenderTextTo(boundsRenderer, text, textOptions); + TextRenderer.RenderTo(boundsRenderer, text, textOptions); canvas.DrawGlyphs(Brushes.Solid(Color.Black), Pens.Solid(Color.Black, 1F), glyphPaths); canvas.Draw(Pens.Solid(Color.Fuchsia.WithAlpha(.5F), 1), boundsRenderer.Boxes); @@ -211,7 +211,7 @@ public void EndText() 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, ReadOnlyMemory intersections) { } diff --git a/tests/SixLabors.Fonts.Tests/TextLayoutTestUtilities.cs b/tests/SixLabors.Fonts.Tests/TextLayoutTestUtilities.cs index 3fc10ace9..e34a9717e 100644 --- a/tests/SixLabors.Fonts.Tests/TextLayoutTestUtilities.cs +++ b/tests/SixLabors.Fonts.Tests/TextLayoutTestUtilities.cs @@ -18,7 +18,6 @@ namespace SixLabors.Fonts.Tests; internal static class TextLayoutTestUtilities { -#if SUPPORTS_DRAWING public static void TestImage( int imageWidth, int imageHeight, @@ -27,12 +26,13 @@ public static void TestImage( [CallerMemberName] string test = "", params object[] properties) { +#if SUPPORTS_DRAWING using Image image = new(Configuration.Default, imageWidth, imageHeight, Color.White.ToPixel()); renderAction(image); image.DebugSave("png", test, properties: properties); image.CompareToReference(percentageTolerance: percentageTolerance, test: test, properties: properties); - } #endif + } public static void TestLayout( string text, @@ -136,6 +136,7 @@ private static RichTextOptions FromTextOptions(TextOptions options, bool customD { RichTextOptions result = new(options.Font) { + FontWeight = options.FontWeight, FallbackFontFamilies = new List(options.FallbackFontFamilies), TabWidth = options.TabWidth, HintingMode = options.HintingMode, @@ -172,6 +173,7 @@ private static RichTextOptions FromTextOptions(TextOptions options, bool customD RichTextRun richRun = new() { Font = run.Font, + FontWeight = run.FontWeight, Start = run.Start, End = run.End, TextAttributes = run.TextAttributes, diff --git a/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs b/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs index ae6163f49..f925cb909 100644 --- a/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs +++ b/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. using System.Globalization; @@ -611,7 +611,7 @@ public void LayoutWithLocation(string text, float x, float y, float expectedX, f GlyphRenderer glyphRenderer = new(); TextRenderer renderer = new(glyphRenderer); - renderer.RenderText( + renderer.Render( text, new TextOptions(new Font(font, 1)) { @@ -1159,6 +1159,7 @@ public void TextPlaceholder_RunMustBeInsertionPoint() Assert.Throws(() => TextMeasurer.MeasureAdvance(text, options)); } +#if SUPPORTS_DRAWING [Theory] [InlineData(TextPlaceholderAlignment.Baseline)] [InlineData(TextPlaceholderAlignment.AboveBaseline)] @@ -1265,7 +1266,9 @@ public void TextPlaceholder_DrawsInlineReservedSpace(TextPlaceholderAlignment al // also exposes one object-replacement glyph at the insertion point. Assert.Equal(1, CountGlyphs(metrics.GetGlyphMetrics().Span, CodePoint.ObjectReplacementChar)); } +#endif +#if SUPPORTS_DRAWING [Theory] [InlineData(TextPlaceholderAlignment.Baseline)] [InlineData(TextPlaceholderAlignment.AboveBaseline)] @@ -1371,6 +1374,7 @@ public void TextPlaceholder_DrawsOversizedInlineReservedSpace(TextPlaceholderAli // The oversized visual still represents one atomic inline object. Assert.Equal(1, CountGlyphs(metrics.GetGlyphMetrics().Span, CodePoint.ObjectReplacementChar)); } +#endif [Theory] [InlineData(LayoutMode.HorizontalTopBottom)] @@ -2956,7 +2960,7 @@ public static List GenerateGlyphRectangles(string text, TextOptio { CaptureGlyphRectangleBuilder glyphBuilder = new(); TextRenderer renderer = new(glyphBuilder); - renderer.RenderText(text, options); + renderer.Render(text, options); return glyphBuilder.GlyphRectangles; } @@ -3014,7 +3018,7 @@ 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, ReadOnlyMemory intersections) { } diff --git a/tests/SixLabors.Fonts.Tests/TextMeasurerGlyphIdTests.cs b/tests/SixLabors.Fonts.Tests/TextMeasurerGlyphIdTests.cs new file mode 100644 index 000000000..ff138ac28 --- /dev/null +++ b/tests/SixLabors.Fonts.Tests/TextMeasurerGlyphIdTests.cs @@ -0,0 +1,362 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.Fonts.Rendering; +using SixLabors.Fonts.Unicode; + +namespace SixLabors.Fonts.Tests; + +/// +/// Verifies that per-glyph measurement matches rendering exactly: the bounds returned by the +/// glyph-id overloads must equal the boxes the renderer reports +/// through for the same input, and the advance and +/// renderable-bounds overloads must compose consistently with the text-level measurements. +/// +public class TextMeasurerGlyphIdTests +{ + [Theory] + [InlineData('A', 0F, 0F, 72F)] + [InlineData('A', 13.5F, 27.25F, 72F)] + [InlineData('g', 100F, 50F, 96F)] + [InlineData(' ', 10F, 10F, 72F)] + public void MeasureBounds_MatchesRenderedGlyphBounds(char character, float originX, float originY, float dpi) + { + Font font = TestFonts.GetFont(TestFonts.OpenSansFile, 32); + ushort glyphId = GetGlyphId(font, character); + + GlyphOptions options = new() + { + Font = font, + Dpi = dpi, + Origin = new Vector2(originX, originY) + }; + + GlyphRenderer renderer = new(); + TextRenderer.RenderTo(renderer, glyphId, options); + + Assert.Single(renderer.GlyphRects); + Assert.Equal(renderer.GlyphRects[0], TextMeasurer.MeasureBounds(glyphId, options)); + } + + [Fact] + public void MeasureGlyph_ReturnsEmpty_ForMissingGlyph() + { + Font font = TestFonts.GetFont(TestFonts.OpenSansFile, 32); + GlyphOptions options = new() + { + Font = font + }; + + Assert.Equal(FontRectangle.Empty, TextMeasurer.MeasureBounds(ushort.MaxValue, options)); + Assert.Equal(FontRectangle.Empty, TextMeasurer.MeasureAdvance(ushort.MaxValue, options)); + Assert.Equal(FontRectangle.Empty, TextMeasurer.MeasureRenderableBounds(ushort.MaxValue, options)); + } + + [Fact] + public void MeasureAdvance_MatchesLaidOutGlyphAdvanceSize() + { + Font font = TestFonts.GetFont(TestFonts.OpenSansFile, 32); + ushort glyphId = GetGlyphId(font, 'A'); + + GlyphOptions options = new() + { + Font = font + }; + + // Layout produces a positioned advance box per glyph; a lone glyph measured without + // layout must occupy the same logical cell (advance width by line height). + GlyphMetrics laidOut = TextMeasurer.GetGlyphMetrics("A", new TextOptions(font)).Span[0]; + FontRectangle measured = TextMeasurer.MeasureAdvance(glyphId, options); + + Assert.Equal(laidOut.Advance.Width, measured.Width, 3F); + Assert.Equal(laidOut.Advance.Height, measured.Height, 3F); + } + + [Fact] + public void MeasureRenderableBounds_IsUnionOfAdvanceAndBounds() + { + Font font = TestFonts.GetFont(TestFonts.OpenSansFile, 32); + ushort glyphId = GetGlyphId(font, 'g'); + + GlyphOptions options = new() + { + Font = font, + Origin = new Vector2(20F, 60F) + }; + + FontRectangle expected = FontRectangle.Union( + TextMeasurer.MeasureAdvance(glyphId, options), + TextMeasurer.MeasureBounds(glyphId, options)); + + Assert.Equal(expected, TextMeasurer.MeasureRenderableBounds(glyphId, options)); + } + + [Fact] + public void MeasureBounds_MatchesUnionOfRenderedGlyphBounds() + { + Font font = TestFonts.GetFont(TestFonts.OpenSansFile, 32); + (GlyphRun glyphRun, GlyphOptions options) = CreateRun(font); + + GlyphRenderer renderer = new(); + TextRenderer.RenderTo(renderer, glyphRun, options); + + Assert.Equal(glyphRun.Count, renderer.GlyphRects.Count); + FontRectangle expected = renderer.GlyphRects[0]; + for (int i = 1; i < renderer.GlyphRects.Count; i++) + { + expected = FontRectangle.Union(expected, renderer.GlyphRects[i]); + } + + Assert.Equal(expected, TextMeasurer.MeasureBounds(glyphRun, options)); + } + + [Fact] + public void MeasureGlyphRun_MatchesUnionOfSingleGlyphMeasurements() + { + Font font = TestFonts.GetFont(TestFonts.OpenSansFile, 32); + (GlyphRun glyphRun, GlyphOptions options) = CreateRun(font); + + FontRectangle expectedAdvance = default; + FontRectangle expectedRenderable = default; + for (int i = 0; i < glyphRun.Count; i++) + { + GlyphOptions positioned = new() + { + Font = font, + Origin = glyphRun.Origins.Span[i] + }; + + ushort glyphId = glyphRun.GlyphIds.Span[i]; + FontRectangle advance = TextMeasurer.MeasureAdvance(glyphId, positioned); + FontRectangle renderable = TextMeasurer.MeasureRenderableBounds(glyphId, positioned); + expectedAdvance = i == 0 ? advance : FontRectangle.Union(expectedAdvance, advance); + expectedRenderable = i == 0 ? renderable : FontRectangle.Union(expectedRenderable, renderable); + } + + Assert.Equal(expectedAdvance, TextMeasurer.MeasureAdvance(glyphRun, options)); + Assert.Equal(expectedRenderable, TextMeasurer.MeasureRenderableBounds(glyphRun, options)); + } + + [Fact] + public void MeasureGlyphRun_RestoresOptionsOrigin() + { + Font font = TestFonts.GetFont(TestFonts.OpenSansFile, 32); + (GlyphRun glyphRun, GlyphOptions options) = CreateRun(font); + options.Origin = new Vector2(5F, 7F); + + _ = TextMeasurer.MeasureBounds(glyphRun, options); + + Assert.Equal(new Vector2(5F, 7F), options.Origin); + } + + [Fact] + public void MeasureGlyphRun_ReturnsEmpty_ForEmptyRun() + { + Font font = TestFonts.GetFont(TestFonts.OpenSansFile, 32); + GlyphRun glyphRun = new(ReadOnlyMemory.Empty, ReadOnlyMemory.Empty); + GlyphOptions options = new() + { + Font = font + }; + + Assert.Equal(FontRectangle.Empty, TextMeasurer.MeasureBounds(glyphRun, options)); + Assert.Equal(FontRectangle.Empty, TextMeasurer.MeasureAdvance(glyphRun, options)); + Assert.Equal(FontRectangle.Empty, TextMeasurer.MeasureRenderableBounds(glyphRun, options)); + } + + [Fact] + public void GetGlyphMetrics_MatchesSingleGlyphMeasurements() + { + Font font = TestFonts.GetFont(TestFonts.OpenSansFile, 32); + ushort glyphId = GetGlyphId(font, 'g'); + + GlyphOptions options = new() + { + Font = font, + Origin = new Vector2(20F, 60F), + GraphemeIndex = 7 + }; + + GlyphMetrics metrics = TextMeasurer.GetGlyphMetrics(glyphId, options); + + Assert.Equal(TextMeasurer.MeasureAdvance(glyphId, options), metrics.Advance); + Assert.Equal(TextMeasurer.MeasureBounds(glyphId, options), metrics.Bounds); + Assert.Equal(TextMeasurer.MeasureRenderableBounds(glyphId, options), metrics.RenderableBounds); + Assert.Equal(new CodePoint('g'), metrics.CodePoint); + Assert.Equal(7, metrics.GraphemeIndex); + Assert.Equal(0, metrics.StringIndex); + Assert.Equal(font, metrics.Font); + } + + [Fact] + public void GetGlyphMetrics_ReturnsOneIndexCorrelatedEntryPerRunGlyph() + { + Font font = TestFonts.GetFont(TestFonts.OpenSansFile, 32); + (GlyphRun glyphRun, GlyphOptions options) = CreateRun(font); + + ReadOnlySpan entries = TextMeasurer.GetGlyphMetrics(glyphRun, options).Span; + + Assert.Equal(glyphRun.Count, entries.Length); + for (int i = 0; i < entries.Length; i++) + { + GlyphOptions positioned = new() + { + Font = font, + Origin = glyphRun.Origins.Span[i] + }; + + ushort glyphId = glyphRun.GlyphIds.Span[i]; + Assert.Equal(TextMeasurer.MeasureAdvance(glyphId, positioned), entries[i].Advance); + Assert.Equal(TextMeasurer.MeasureBounds(glyphId, positioned), entries[i].Bounds); + Assert.Equal(TextMeasurer.MeasureRenderableBounds(glyphId, positioned), entries[i].RenderableBounds); + Assert.Equal(i, entries[i].GraphemeIndex); + Assert.Equal(i, entries[i].StringIndex); + } + } + + [Fact] + public void GetGlyphMetrics_ReturnsEmptyRectangles_ForMissingRunGlyph() + { + Font font = TestFonts.GetFont(TestFonts.OpenSansFile, 32); + ushort[] glyphIds = [GetGlyphId(font, 'A'), ushort.MaxValue, GetGlyphId(font, 'B')]; + Vector2[] origins = [new(0F, 40F), new(25F, 40F), new(50F, 40F)]; + GlyphRun glyphRun = new(glyphIds, origins); + GlyphOptions options = new() + { + Font = font + }; + + ReadOnlySpan entries = TextMeasurer.GetGlyphMetrics(glyphRun, options).Span; + + Assert.Equal(3, entries.Length); + Assert.NotEqual(FontRectangle.Empty, entries[0].Bounds); + Assert.Equal(FontRectangle.Empty, entries[1].Advance); + Assert.Equal(FontRectangle.Empty, entries[1].Bounds); + Assert.Equal(FontRectangle.Empty, entries[1].RenderableBounds); + Assert.NotEqual(FontRectangle.Empty, entries[2].Bounds); + } + + + [Fact] + public void GetIntersections_DescenderBand_IsNarrowerThanInkBounds() + { + Font font = TestFonts.GetFont(TestFonts.OpenSansFile, 64); + ushort glyphId = GetGlyphId(font, 'p'); + + GlyphOptions options = new() + { + Font = font, + Origin = new Vector2(10F, 100F) + }; + + FontRectangle bounds = TextMeasurer.MeasureBounds(glyphId, options); + + // A thin band just below the baseline crosses only the descender stem. + ReadOnlySpan intersections = TextMeasurer.GetIntersections(glyphId, options, 103F, 106F).Span; + + Assert.True(intersections.Length >= 2); + Assert.True(intersections.Length % 2 == 0); + + float coveredWidth = 0F; + for (int i = 0; i < intersections.Length; i += 2) + { + Assert.True(intersections[i] <= intersections[i + 1]); + Assert.True(intersections[i] >= bounds.Left - 1F); + Assert.True(intersections[i + 1] <= bounds.Right + 1F); + coveredWidth += intersections[i + 1] - intersections[i]; + } + + // The stem is a small fraction of the glyph's full ink width; the box approximation + // this API replaces would report the full width. + Assert.True(coveredWidth < bounds.Width * 0.5F); + } + + [Fact] + public void GetIntersections_BandOutsideInk_ReturnsEmpty() + { + Font font = TestFonts.GetFont(TestFonts.OpenSansFile, 64); + ushort glyphId = GetGlyphId(font, 'o'); + + GlyphOptions options = new() + { + Font = font, + Origin = new Vector2(10F, 100F) + }; + + // 'o' has no descender; a band below the baseline never touches its outline. + Assert.True(TextMeasurer.GetIntersections(glyphId, options, 105F, 110F).IsEmpty); + } + + [Fact] + public void GetIntersections_Run_MatchesUnionOfSingleGlyphIntersections() + { + Font font = TestFonts.GetFont(TestFonts.OpenSansFile, 64); + ushort glyphId = GetGlyphId(font, 'p'); + Vector2[] origins = [new(10F, 100F), new(60F, 100F)]; + GlyphRun glyphRun = new(new[] { glyphId, glyphId }, origins); + GlyphOptions options = new() + { + Font = font + }; + + ReadOnlySpan run = TextMeasurer.GetIntersections(glyphRun, options, 103F, 106F).Span; + + GlyphOptions first = new() { Font = font, Origin = origins[0] }; + GlyphOptions second = new() { Font = font, Origin = origins[1] }; + ReadOnlySpan a = TextMeasurer.GetIntersections(glyphId, first, 103F, 106F).Span; + ReadOnlySpan b = TextMeasurer.GetIntersections(glyphId, second, 103F, 106F).Span; + + // The glyphs are far apart, so the run result is the concatenation of the singles. + Assert.Equal(a.Length + b.Length, run.Length); + for (int i = 0; i < a.Length; i++) + { + Assert.Equal(a[i], run[i], 2F); + } + + for (int i = 0; i < b.Length; i++) + { + Assert.Equal(b[i], run[a.Length + i], 2F); + } + } + + /// + /// Creates a small positioned run with fractional origins and a whitespace glyph. + /// + /// The font to resolve glyph ids against. + /// The positioned run and matching options. + private static (GlyphRun GlyphRun, GlyphOptions Options) CreateRun(Font font) + { + ushort[] glyphIds = + [ + GetGlyphId(font, 'H'), + GetGlyphId(font, 'e'), + GetGlyphId(font, 'y'), + GetGlyphId(font, ' '), + GetGlyphId(font, '!') + ]; + + Vector2[] origins = + [ + new(10F, 40F), + new(33.5F, 40F), + new(52F, 42.25F), + new(70F, 40F), + new(80F, 38F) + ]; + + return (new GlyphRun(glyphIds, origins), new GlyphOptions { Font = font }); + } + + /// + /// Resolves the glyph id for a character through the public glyph lookup. + /// + /// The font to resolve against. + /// The character to resolve. + /// The glyph id. + private static ushort GetGlyphId(Font font, char character) + { + Assert.True(font.TryGetGlyphs(new CodePoint(character), out Glyph? glyph)); + return glyph.Value.GlyphMetrics.GlyphId; + } +} diff --git a/tests/SixLabors.Fonts.Tests/TextOptionsTests.cs b/tests/SixLabors.Fonts.Tests/TextOptionsTests.cs index ff4fbd879..8da4573ed 100644 --- a/tests/SixLabors.Fonts.Tests/TextOptionsTests.cs +++ b/tests/SixLabors.Fonts.Tests/TextOptionsTests.cs @@ -191,7 +191,7 @@ public void GetMissingGlyphFromMainFont() ReadOnlySpan text = "Z".AsSpan(); GlyphRenderer renderer = new(); - TextRenderer.RenderTextTo(renderer, text, options); + TextRenderer.RenderTo(renderer, text, options); GlyphRendererParameters glyph = Assert.Single(renderer.GlyphKeys); Assert.Equal(GlyphType.Fallback, glyph.GlyphType); @@ -219,7 +219,7 @@ public void GetGlyphFromFirstAvailableInstance(char character, string instance) ReadOnlySpan text = [character]; GlyphRenderer renderer = new(); - TextRenderer.RenderTextTo(renderer, text, options); + TextRenderer.RenderTo(renderer, text, options); GlyphRendererParameters glyph = Assert.Single(renderer.GlyphKeys); Assert.Equal(GlyphType.Standard, glyph.GlyphType); Fakes.FakeFontInstance expectedInstance = instance switch diff --git a/tests/SixLabors.Fonts.Tests/TextRendererGlyphIdTests.cs b/tests/SixLabors.Fonts.Tests/TextRendererGlyphIdTests.cs new file mode 100644 index 000000000..c92c833c1 --- /dev/null +++ b/tests/SixLabors.Fonts.Tests/TextRendererGlyphIdTests.cs @@ -0,0 +1,173 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.Fonts.Rendering; +using SixLabors.Fonts.Unicode; + +namespace SixLabors.Fonts.Tests; + +public class TextRendererGlyphIdTests +{ + [Fact] + public void TryGetGlyphMetrics_GlyphId_MatchesCodePointPath() + { + Font font = TestFonts.GetFont(TestFonts.SimpleFontFile, 12); + CodePoint codePoint = new('A'); + + Assert.True(font.TryGetGlyphs(codePoint, out Glyph? glyph)); + ushort glyphId = glyph.Value.GlyphMetrics.GlyphId; + + Assert.True(font.FontMetrics.TryGetGlyphMetrics( + codePoint, + TextAttributes.None, + TextDecorations.None, + LayoutMode.HorizontalTopBottom, + ColorFontSupport.None, + out FontGlyphMetrics codePointMetrics)); + + Assert.True(font.FontMetrics.TryGetGlyphMetrics( + glyphId, + TextAttributes.None, + TextDecorations.None, + LayoutMode.HorizontalTopBottom, + ColorFontSupport.None, + out FontGlyphMetrics glyphIdMetrics)); + + Assert.Equal(codePointMetrics.GlyphId, glyphIdMetrics.GlyphId); + Assert.Equal(codePointMetrics.CodePoint, glyphIdMetrics.CodePoint); + Assert.Equal(codePointMetrics.AdvanceWidth, glyphIdMetrics.AdvanceWidth); + Assert.Equal(codePointMetrics.AdvanceHeight, glyphIdMetrics.AdvanceHeight); + } + + [Fact] + public void RenderGlyph_GlyphId_MatchesStringRenderingForColorGlyph() + { + FontFamily family = TestFonts.GetFontFamily(TestFonts.NotoColorEmojiRegular); + Font font = family.CreateFont(128); + CodePoint codePoint = new(0x1F638); // Grinning cat face with smiling eyes. + + Assert.True(font.TryGetGlyphs(codePoint, ColorFontSupport.ColrV1, out Glyph? glyph)); + + TextOptions textOptions = new(font) + { + ColorFontSupport = ColorFontSupport.ColrV1 + }; + + LayerCaptureRenderer textRenderer = new(); + TextRenderer.RenderTo(textRenderer, char.ConvertFromUtf32(codePoint.Value), textOptions); + + GlyphOptions glyphOptions = new() + { + Font = font, + ColorFontSupport = ColorFontSupport.ColrV1 + }; + + LayerCaptureRenderer glyphIdRenderer = new(); + TextRenderer.RenderTo(glyphIdRenderer, glyph.Value.GlyphMetrics.GlyphId, glyphOptions); + + Assert.Equal(textRenderer.GlyphKeys.Count, glyphIdRenderer.GlyphKeys.Count); + Assert.Equal(textRenderer.GlyphRects[0].Width, glyphIdRenderer.GlyphRects[0].Width); + Assert.Equal(textRenderer.GlyphRects[0].Height, glyphIdRenderer.GlyphRects[0].Height); + Assert.Equal(textRenderer.SolidLayers, glyphIdRenderer.SolidLayers); + Assert.Equal(textRenderer.GlyphKeys[0].GlyphId, glyphIdRenderer.GlyphKeys[0].GlyphId); + Assert.Equal(textRenderer.GlyphKeys[0].CodePoint, glyphIdRenderer.GlyphKeys[0].CodePoint); + } + + [Fact] + public void RenderGlyph_UsesTextRunCreatedByGlyphOptions() + { + Font font = TestFonts.GetFont(TestFonts.SimpleFontFile, 12); + CodePoint codePoint = new('A'); + + Assert.True(font.TryGetGlyphs(codePoint, out Glyph? glyph)); + + CustomGlyphOptions options = new() + { + Font = font, + GraphemeIndex = 42, + TextAttributes = TextAttributes.Superscript, + TextDecorations = TextDecorations.Underline + }; + + GlyphRenderer renderer = new(); + TextRenderer.RenderTo(renderer, glyph.Value.GlyphMetrics.GlyphId, options); + + GlyphRendererParameters parameters = Assert.Single(renderer.GlyphKeys); + CustomTextRun run = Assert.IsType(parameters.TextRun); + Assert.Equal(42, parameters.GraphemeIndex); + Assert.Equal(42, run.Start); + Assert.Equal(43, run.End); + Assert.Equal(TextAttributes.Superscript, run.TextAttributes); + Assert.Equal(TextDecorations.Underline, run.TextDecorations); + } + + [Fact] + public void RenderGlyph_UnknownGlyphId_DoesNotRender() + { + Font font = TestFonts.GetFont(TestFonts.SimpleFontFile, 12); + GlyphOptions glyphOptions = new() + { + Font = font + }; + + GlyphRenderer renderer = new(); + TextRenderer.RenderTo(renderer, ushort.MaxValue, glyphOptions); + + Assert.Empty(renderer.GlyphKeys); + Assert.Empty(renderer.ControlPoints); + } + + [Fact] + public void RenderGlyph_ColorGlyphById_EmitsPaintedLayers() + { + FontFamily family = TestFonts.GetFontFamily(TestFonts.NotoColorEmojiRegular); + Font font = family.CreateFont(128); + CodePoint codePoint = new(0x1F638); // Grinning cat face with smiling eyes. + + Assert.True(font.TryGetGlyphs(codePoint, ColorFontSupport.ColrV1, out Glyph? glyph)); + + GlyphOptions glyphOptions = new() + { + Font = font, + ColorFontSupport = ColorFontSupport.ColrV1 + }; + + LayerCaptureRenderer renderer = new(); + TextRenderer.RenderTo(renderer, glyph.Value.GlyphMetrics.GlyphId, glyphOptions); + + Assert.Single(renderer.GlyphKeys); + Assert.NotEmpty(renderer.SolidLayers); + } + + private sealed class CustomGlyphOptions : GlyphOptions + { + protected internal override TextRun CreateTextRun() + => new CustomTextRun + { + Start = this.GraphemeIndex, + End = this.GraphemeIndex + 1, + Font = this.Font, + TextAttributes = this.TextAttributes, + TextDecorations = this.TextDecorations + }; + } + + private sealed class CustomTextRun : TextRun + { + } + + private sealed class LayerCaptureRenderer : GlyphRenderer + { + public List SolidLayers { get; } = []; + + public override void BeginLayer(Paint paint, FillRule fillRule, ClipQuad? clipBounds) + { + if (paint is SolidPaint solidPaint) + { + this.SolidLayers.Add(solidPaint.Color); + } + + base.BeginLayer(paint, fillRule, clipBounds); + } + } +} diff --git a/tests/SixLabors.Fonts.Tests/TrueTypeCollectionTests.cs b/tests/SixLabors.Fonts.Tests/TrueTypeCollectionTests.cs index 347d636f6..8c47c797e 100644 --- a/tests/SixLabors.Fonts.Tests/TrueTypeCollectionTests.cs +++ b/tests/SixLabors.Fonts.Tests/TrueTypeCollectionTests.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Globalization; +using SixLabors.Fonts.Unicode; namespace SixLabors.Fonts.Tests; @@ -54,4 +55,22 @@ public void AddViaStreamReturnsDescription() FontDescription openSansDescription = Assert.Single(descriptionArray, x => x.FontNameInvariantCulture == "Open Sans"); FontDescription abFontDescription = Assert.Single(descriptionArray, x => x.FontNameInvariantCulture == "SixLaborsSampleAB regular"); } + + [Fact] + public void AddViaStreamCanUseFontsAfterSourceDisposed() + { + FontCollection sut = new(); + FontFamily[] families; + + using (Stream stream = TestFonts.SSimpleTrueTypeCollectionData()) + { + families = sut.AddCollection(stream).ToArray(); + } + + FontFamily openSans = Assert.Single(families, x => x.Name == "Open Sans"); + Font font = openSans.CreateFont(12); + + Assert.True(font.TryGetGlyphId(new CodePoint('A'), out ushort glyphId)); + Assert.NotEqual(0, glyphId); + } } diff --git a/tests/SixLabors.Fonts.Tests/Unicode/BidiAlgorithmTests.cs b/tests/SixLabors.Fonts.Tests/Unicode/BidiAlgorithmTests.cs index 057efa609..915cc42df 100644 --- a/tests/SixLabors.Fonts.Tests/Unicode/BidiAlgorithmTests.cs +++ b/tests/SixLabors.Fonts.Tests/Unicode/BidiAlgorithmTests.cs @@ -28,7 +28,7 @@ public void RendersKurdishTextCorrect() ]; // act - TextRenderer.RenderTextTo(renderer, testStr, new TextOptions(arabicFont)); + TextRenderer.RenderTo(renderer, testStr, new TextOptions(arabicFont)); // assert Assert.Equal(expectedGlyphIndices.Length, renderer.GlyphKeys.Count); @@ -52,7 +52,7 @@ public void RendersFarsiTextCorrect() ]; // act - TextRenderer.RenderTextTo(renderer, testStr, new TextOptions(arabicFont)); + TextRenderer.RenderTo(renderer, testStr, new TextOptions(arabicFont)); // assert Assert.Equal(expectedGlyphIndices.Length, renderer.GlyphKeys.Count); @@ -77,7 +77,7 @@ public void RendersArabicTextWithPunctuationCorrectly() ]; // act - TextRenderer.RenderTextTo(renderer, testStr, new TextOptions(arabicFont)); + TextRenderer.RenderTo(renderer, testStr, new TextOptions(arabicFont)); // assert Assert.Equal(expectedGlyphIndices.Length, renderer.GlyphKeys.Count); @@ -97,7 +97,7 @@ public void RendersArabicNumbersFromLeftToRight() int[] expectedGlyphIndices = [403, 405, 407, 409, 411, 413, 415, 417, 419, 421]; // act - TextRenderer.RenderTextTo(renderer, testStr, new TextOptions(arabicFont)); + TextRenderer.RenderTo(renderer, testStr, new TextOptions(arabicFont)); // assert Assert.Equal(expectedGlyphIndices.Length, renderer.GlyphKeys.Count); @@ -117,7 +117,7 @@ public void MixingArabicWordsWithNumbers_Works() int[] expectedGlyphIndices = [2317, 3631, 2380, 2345, 2345, 2485, 3, 2264, 2265, 2266, 2267, 3, 2379, 2540, 2247, 2260, 3, 2842, 2286]; // act - TextRenderer.RenderTextTo(renderer, testStr, new TextOptions(arabicFont)); + TextRenderer.RenderTo(renderer, testStr, new TextOptions(arabicFont)); // assert Assert.Equal(expectedGlyphIndices.Length, renderer.GlyphKeys.Count); @@ -137,7 +137,7 @@ public void MathematicalFormulasWithArabicText_Works() int[] expectedGlyphIndices = [2271, 2268, 2264, 3, 32, 3, 2322, 2271, 3, 14, 3, 2329, 2264, 2264]; // act - TextRenderer.RenderTextTo(renderer, testStr, new TextOptions(arabicFont)); + TextRenderer.RenderTo(renderer, testStr, new TextOptions(arabicFont)); // assert Assert.Equal(expectedGlyphIndices.Length, renderer.GlyphKeys.Count);