From 4f9f9925a11b9abf156af29521dc00d3da4e3443 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 2 Mar 2022 12:45:09 +1100 Subject: [PATCH 01/48] Initial implementation --- samples/DrawWithImageSharp/Program.cs | 18 ++- .../GlyphPositioningCollection.cs | 9 +- .../GlyphSubstitutionCollection.cs | 6 +- src/SixLabors.Fonts/TextAttribute.cs | 39 ++++++ src/SixLabors.Fonts/TextLayout.cs | 128 +++++++++++++----- src/SixLabors.Fonts/TextOptions.cs | 10 +- src/SixLabors.Fonts/TextRun.cs | 68 ++++++++++ 7 files changed, 236 insertions(+), 42 deletions(-) create mode 100644 src/SixLabors.Fonts/TextAttribute.cs create mode 100644 src/SixLabors.Fonts/TextRun.cs diff --git a/samples/DrawWithImageSharp/Program.cs b/samples/DrawWithImageSharp/Program.cs index f1735e38d..0fbfeecea 100644 --- a/samples/DrawWithImageSharp/Program.cs +++ b/samples/DrawWithImageSharp/Program.cs @@ -40,6 +40,14 @@ public static void Main(string[] args) FontFamily arabicFont = SystemFonts.Get("Dubai"); FontFamily tahoma = SystemFonts.Get("Tahoma"); + var textRuns = new List + { + new TextRun { Start = 4, End = 9, Font = uiFont.CreateFont(32, FontStyle.Bold) }, + new TextRun { Start = 26, End = 30, Font = uiFont.CreateFont(32, FontStyle.Italic) } + }; + + RenderText(uiFont, "The quick brown fox jumps over the lazy dog", 32, textRuns: textRuns); + return; RenderText(font2, "\uFB01", pointSize: 11.25F); RenderText(fontWoff2, "\uFB01", pointSize: 11.25F); RenderText(tahoma, "p", pointSize: 11.25F); @@ -182,12 +190,18 @@ public static void RenderText(TextOptions options, string text) SaveImage(options, text, (int)size.Width, (int)size.Height, options.Font.Name, text + ".png"); } - public static void RenderText(FontFamily font, string text, float pointSize = 12, IEnumerable fallbackFonts = null) + public static void RenderText( + FontFamily font, + string text, + float pointSize = 12, + IEnumerable fallbackFonts = null, + IEnumerable textRuns = null) => RenderText( new TextOptions(new Font(font, pointSize)) { WrappingLength = 400, - FallbackFontFamilies = fallbackFonts?.ToArray() + FallbackFontFamilies = fallbackFonts?.ToArray(), + TextRuns = textRuns?.ToArray() }, text); diff --git a/src/SixLabors.Fonts/GlyphPositioningCollection.cs b/src/SixLabors.Fonts/GlyphPositioningCollection.cs index 50b42378f..fc4a3b715 100644 --- a/src/SixLabors.Fonts/GlyphPositioningCollection.cs +++ b/src/SixLabors.Fonts/GlyphPositioningCollection.cs @@ -92,8 +92,11 @@ public bool TryGetGlyphMetricsAtOffset(int offset, [NotNullWhen(true)] out Glyph /// /// The font face with metrics. /// The glyph substitution collection. + /// The start index for the given collection. + /// The end index for the given collection. + /// Whether the current font run is a sliced text run. /// if the metrics collection does not contain any fallbacks; otherwise . - public bool TryAddOrUpdate(FontMetrics fontMetrics, GlyphSubstitutionCollection collection) + public bool TryAddOrUpdate(FontMetrics fontMetrics, GlyphSubstitutionCollection collection, int start, int end, bool textRun) { if (this.Count == 0) { @@ -102,7 +105,7 @@ public bool TryAddOrUpdate(FontMetrics fontMetrics, GlyphSubstitutionCollection bool hasFallBacks = false; List orphans = new(); - for (int i = 0; i < this.offsets.Count; i++) + for (int i = start; i < Math.Min(end, this.offsets.Count); i++) { int offset = this.offsets[i]; if (!collection.TryGetGlyphShapingDataAtOffset(offset, out GlyphShapingData? data)) @@ -114,7 +117,7 @@ public bool TryAddOrUpdate(FontMetrics fontMetrics, GlyphSubstitutionCollection } GlyphMetrics[] metrics = this.map[offset]; - if (metrics[0].GlyphType != GlyphType.Fallback) + if (!textRun && metrics[0].GlyphType != GlyphType.Fallback) { // We've already got the correct glyph. continue; diff --git a/src/SixLabors.Fonts/GlyphSubstitutionCollection.cs b/src/SixLabors.Fonts/GlyphSubstitutionCollection.cs index e1aac13ba..6fe5c8292 100644 --- a/src/SixLabors.Fonts/GlyphSubstitutionCollection.cs +++ b/src/SixLabors.Fonts/GlyphSubstitutionCollection.cs @@ -41,7 +41,7 @@ public GlyphSubstitutionCollection(TextOptions textOptions) public int Count => this.offsets.Count; /// - public bool IsVerticalLayoutMode { get; private set; } + public bool IsVerticalLayoutMode { get; } /// public TextOptions TextOptions { get; } @@ -109,13 +109,11 @@ public void AddGlyph(ushort glyphId, CodePoint codePoint, TextDirection directio /// /// Removes all elements from the collection. /// - /// The text layout mode. - public void Clear(LayoutMode mode) + public void Clear() { this.offsets.Clear(); this.glyphs.Clear(); this.LigatureId = 1; - this.IsVerticalLayoutMode = mode.IsVertical(); } /// diff --git a/src/SixLabors.Fonts/TextAttribute.cs b/src/SixLabors.Fonts/TextAttribute.cs new file mode 100644 index 000000000..a6ba01718 --- /dev/null +++ b/src/SixLabors.Fonts/TextAttribute.cs @@ -0,0 +1,39 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; + +namespace SixLabors.Fonts +{ + /// + /// Provides enumeration of various text attributes. + /// + [Flags] + public enum TextAttribute + { + /// + /// No attributes are applied + /// + None = 0, + + /// + /// The text is underlined + /// + Underline = 1 << 0, + + /// + /// The text contains a horizontal line through the center. + /// + Strikethrough = 1 << 1, + + /// + /// The text set slightly below the normal line of type. + /// + Subscript = 1 << 2, + + /// + /// The text set slightly above the normal line of type. + /// + Superscript = 1 << 3, + } +} diff --git a/src/SixLabors.Fonts/TextLayout.cs b/src/SixLabors.Fonts/TextLayout.cs index 90604c347..109681fa0 100644 --- a/src/SixLabors.Fonts/TextLayout.cs +++ b/src/SixLabors.Fonts/TextLayout.cs @@ -25,18 +25,6 @@ public IReadOnlyList GenerateLayout(ReadOnlySpan text, TextOp return Array.Empty(); } - if (options.WrappingLength > 0) - { - // Trim trailing white spaces from the text - text = text.TrimEnd(null); - } - - // Check our string again after trimming. - if (text.IsEmpty) - { - return Array.Empty(); - } - TextBox textBox = ProcessText(text, options); return LayoutText(textBox, options); } @@ -45,17 +33,9 @@ private static TextBox ProcessText(ReadOnlySpan text, TextOptions options) { // Gather the font and fallbacks. FontMetrics mainFont = options.Font.FontMetrics; - FontMetrics[] fallbackFonts; - if (options.FallbackFontFamilies?.Count > 0) - { - fallbackFonts = options.FallbackFontFamilies - .Select(x => new Font(x, options.Font.Size, options.Font.RequestedStyle).FontMetrics) - .ToArray(); - } - else - { - fallbackFonts = Array.Empty(); - } + FontMetrics[] fallbackFonts = (options.FallbackFontFamilies?.Count > 0) + ? options.FallbackFontFamilies.Select(x => new Font(x, options.Font.Size, options.Font.RequestedStyle).FontMetrics).ToArray() + : Array.Empty(); LayoutMode layoutMode = options.LayoutMode; GlyphSubstitutionCollection substitutions = new(options); @@ -84,21 +64,51 @@ private static TextBox ProcessText(ReadOnlySpan text, TextOptions options) // Incrementally build out collection of glyphs. // For each run we start with a fresh substitution collection to avoid // overwriting the glyph ids. - if (!DoFontRun( + IReadOnlyList textRuns = options.TextRuns ?? Array.Empty(); + int count = CodePoint.GetCodePointCount(text); + + // First do a complete run using our default font. + // This builds our position collection for overwriting. + bool complete = DoFontRun( text, - options, + 0, + count, + false, mainFont, bidiRuns, bidiMap, substitutions, - positionings)) + positionings); + + // Now overwrite with individual text runs. + foreach (TextRun textRun in textRuns) + { + FontMetrics runFont = textRun.Font?.FontMetrics ?? mainFont; + ReadOnlySpan runText = textRun.Slice(text); + substitutions.Clear(); + complete |= DoFontRun( + runText, + textRun.Start, + textRun.End, + true, + runFont, + bidiRuns, + bidiMap, + substitutions, + positionings); + } + + if (!complete) { + // Finally try our fallback fonts. foreach (FontMetrics font in fallbackFonts) { - substitutions.Clear(layoutMode); + substitutions.Clear(); if (DoFontRun( text, - options, + 0, + count, + false, font, bidiRuns, bidiMap, @@ -114,6 +124,13 @@ private static TextBox ProcessText(ReadOnlySpan text, TextOptions options) // Each set of metrics is associated with single font and will only be updated // by that font so it's safe to use a single collection. mainFont.UpdatePositions(positionings); + + foreach (TextRun textRun in textRuns) + { + FontMetrics runFont = textRun.Font?.FontMetrics ?? mainFont; + runFont.UpdatePositions(positionings); + } + foreach (FontMetrics font in fallbackFonts) { font.UpdatePositions(positionings); @@ -122,6 +139,53 @@ private static TextBox ProcessText(ReadOnlySpan text, TextOptions options) return BreakLines(text, options, bidiRuns, bidiMap, positionings, layoutMode); } + private static IReadOnlyList BuildTextRuns(ReadOnlySpan text, TextOptions options) + { + if (options.TextRuns?.Count == 0) + { + return new TextRun[] + { + new() + { + Start = 0, + End = CodePoint.GetCodePointCount(text), + Font = options.Font + } + }; + } + + int start = 0; + int end = CodePoint.GetCodePointCount(text); + List textRuns = new(); + foreach (TextRun textRun in options.TextRuns!.OrderBy(x => x.Start)) + { + // Fill gaps within runs. + if (textRun.Start > start) + { + //textRuns.Add(new() + //{ + // Start = start, + // End = textRun.Start, + //}); + } + + textRuns.Add(textRun); + start = textRun.End; + } + + // Add a final run. + if (start < end) + { + //textRuns.Add(new() + //{ + // Start = start, + // End = end, + //}); + } + + return textRuns; + } + private static IReadOnlyList LayoutText(TextBox textBox, TextOptions options) { LayoutMode layoutMode = options.LayoutMode; @@ -390,7 +454,9 @@ private static IEnumerable LayoutLineVertical( private static bool DoFontRun( ReadOnlySpan text, - TextOptions options, + int start, + int end, + bool textRun, FontMetrics fontMetrics, BidiRun[] bidiRuns, Dictionary bidiMap, @@ -399,7 +465,7 @@ private static bool DoFontRun( { // Enumerate through each grapheme in the text. int graphemeIndex; - int codePointIndex = 0; + int codePointIndex = start; int bidiRun = 0; var graphemeEnumerator = new SpanGraphemeEnumerator(text); for (graphemeIndex = 0; graphemeEnumerator.MoveNext(); graphemeIndex++) @@ -450,7 +516,7 @@ private static bool DoFontRun( SubstituteBidiMirrors(fontMetrics, substitutions); fontMetrics.ApplySubstitution(substitutions); - return positionings.TryAddOrUpdate(fontMetrics, substitutions); + return positionings.TryAddOrUpdate(fontMetrics, substitutions, start, end, textRun); } private static void SubstituteBidiMirrors(FontMetrics fontMetrics, GlyphSubstitutionCollection collection) diff --git a/src/SixLabors.Fonts/TextOptions.cs b/src/SixLabors.Fonts/TextOptions.cs index a99f407eb..dbfeb641f 100644 --- a/src/SixLabors.Fonts/TextOptions.cs +++ b/src/SixLabors.Fonts/TextOptions.cs @@ -9,9 +9,9 @@ namespace SixLabors.Fonts { /// - /// Provides configuration options for rendering text. + /// Provides configuration options for rendering and shaping text. /// - public sealed class TextOptions + public class TextOptions { private float tabWidth = 4F; private float dpi = 72F; @@ -48,6 +48,7 @@ public TextOptions(TextOptions options) this.KerningMode = options.KerningMode; this.ColorFontSupport = options.ColorFontSupport; this.FeatureTags = new List(options.FeatureTags); + this.TextRuns = new List(options.TextRuns); } /// @@ -181,5 +182,10 @@ public float LineSpacing /// Gets or sets the collection of additional feature tags to apply during glyph shaping. /// public IReadOnlyList FeatureTags { get; set; } = Array.Empty(); + + /// + /// Gets or sets an optional collection of text runs to apply to the body of text. + /// + public IReadOnlyList TextRuns { get; set; } = Array.Empty(); } } diff --git a/src/SixLabors.Fonts/TextRun.cs b/src/SixLabors.Fonts/TextRun.cs new file mode 100644 index 000000000..5188b3bef --- /dev/null +++ b/src/SixLabors.Fonts/TextRun.cs @@ -0,0 +1,68 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using SixLabors.Fonts.Unicode; + +namespace SixLabors.Fonts +{ + /// + /// Represents a run of text spanning a series of code points within a string. + /// + public class TextRun + { + /// + /// Gets or sets the inclusive start index of the first codepoint in this . + /// + public int Start { get; set; } + + /// + /// Gets or sets the exclusive end index of the last codepoint in this . + /// + public int End { get; set; } + + /// + /// Gets or sets the font for this run. + /// + public Font? Font { get; set; } + + /// + /// Gets or sets the text attributes applied to this run. + /// + public TextAttribute TextAttributes { get; set; } + + /// + /// Returns the slice of the given text representing this . + /// + /// The text to slice. + /// The . + public ReadOnlySpan Slice(ReadOnlySpan text) + { + // Convert code point indices into char indices so we can slice + int chars = 0; + int count = 0; + int start = 0; + int length = 0; + SpanCodePointEnumerator codePointEnumerator = new(text); + while (codePointEnumerator.MoveNext()) + { + if (count == this.Start) + { + start = chars; + } + + chars += codePointEnumerator.Current.Utf16SequenceLength; + length = chars - start; + if (++count == this.End) + { + break; + } + } + + return text.Slice(start, length); + } + + /// + public override string ToString() => $"[TextRun: Start={this.Start}, End={this.End}, TextAttributes={this.TextAttributes}]"; + } +} From 2ceb25a83411b19195f806e4df477d505e49a2c2 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 2 Mar 2022 13:04:31 +1100 Subject: [PATCH 02/48] Fix bool assignment --- src/SixLabors.Fonts/TextLayout.cs | 54 +++---------------------------- src/SixLabors.Fonts/TextRun.cs | 3 +- 2 files changed, 7 insertions(+), 50 deletions(-) diff --git a/src/SixLabors.Fonts/TextLayout.cs b/src/SixLabors.Fonts/TextLayout.cs index 109681fa0..74bffe001 100644 --- a/src/SixLabors.Fonts/TextLayout.cs +++ b/src/SixLabors.Fonts/TextLayout.cs @@ -86,7 +86,7 @@ private static TextBox ProcessText(ReadOnlySpan text, TextOptions options) FontMetrics runFont = textRun.Font?.FontMetrics ?? mainFont; ReadOnlySpan runText = textRun.Slice(text); substitutions.Clear(); - complete |= DoFontRun( + if (!DoFontRun( runText, textRun.Start, textRun.End, @@ -95,7 +95,10 @@ private static TextBox ProcessText(ReadOnlySpan text, TextOptions options) bidiRuns, bidiMap, substitutions, - positionings); + positionings)) + { + complete = false; + } } if (!complete) @@ -139,53 +142,6 @@ private static TextBox ProcessText(ReadOnlySpan text, TextOptions options) return BreakLines(text, options, bidiRuns, bidiMap, positionings, layoutMode); } - private static IReadOnlyList BuildTextRuns(ReadOnlySpan text, TextOptions options) - { - if (options.TextRuns?.Count == 0) - { - return new TextRun[] - { - new() - { - Start = 0, - End = CodePoint.GetCodePointCount(text), - Font = options.Font - } - }; - } - - int start = 0; - int end = CodePoint.GetCodePointCount(text); - List textRuns = new(); - foreach (TextRun textRun in options.TextRuns!.OrderBy(x => x.Start)) - { - // Fill gaps within runs. - if (textRun.Start > start) - { - //textRuns.Add(new() - //{ - // Start = start, - // End = textRun.Start, - //}); - } - - textRuns.Add(textRun); - start = textRun.End; - } - - // Add a final run. - if (start < end) - { - //textRuns.Add(new() - //{ - // Start = start, - // End = end, - //}); - } - - return textRuns; - } - private static IReadOnlyList LayoutText(TextBox textBox, TextOptions options) { LayoutMode layoutMode = options.LayoutMode; diff --git a/src/SixLabors.Fonts/TextRun.cs b/src/SixLabors.Fonts/TextRun.cs index 5188b3bef..ed7227fb4 100644 --- a/src/SixLabors.Fonts/TextRun.cs +++ b/src/SixLabors.Fonts/TextRun.cs @@ -63,6 +63,7 @@ public ReadOnlySpan Slice(ReadOnlySpan text) } /// - public override string ToString() => $"[TextRun: Start={this.Start}, End={this.End}, TextAttributes={this.TextAttributes}]"; + public override string ToString() + => $"[TextRun: Start={this.Start}, End={this.End}, TextAttributes={this.TextAttributes}]"; } } From 2ef235343acd427820aeec3523000500324099d5 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 2 Mar 2022 18:04:27 +1100 Subject: [PATCH 03/48] Trim down to a max of two runs --- .../GlyphPositioningCollection.cs | 26 ++--- src/SixLabors.Fonts/TextLayout.cs | 109 ++++++++++++------ 2 files changed, 85 insertions(+), 50 deletions(-) diff --git a/src/SixLabors.Fonts/GlyphPositioningCollection.cs b/src/SixLabors.Fonts/GlyphPositioningCollection.cs index fc4a3b715..151ee94c0 100644 --- a/src/SixLabors.Fonts/GlyphPositioningCollection.cs +++ b/src/SixLabors.Fonts/GlyphPositioningCollection.cs @@ -86,26 +86,17 @@ public bool TryGetGlyphMetricsAtOffset(int offset, [NotNullWhen(true)] out Glyph => this.map.TryGetValue(offset, out metrics); /// - /// Adds the collection of glyph ids to the metrics collection. - /// Adding subsequent collections will overwrite any glyphs that have been previously + /// Updates the collection of glyph ids to the metrics collection to overwrite any glyphs that have been previously /// identified as fallbacks. /// /// The font face with metrics. /// The glyph substitution collection. - /// The start index for the given collection. - /// The end index for the given collection. - /// Whether the current font run is a sliced text run. /// if the metrics collection does not contain any fallbacks; otherwise . - public bool TryAddOrUpdate(FontMetrics fontMetrics, GlyphSubstitutionCollection collection, int start, int end, bool textRun) + public bool TryUpdate(FontMetrics fontMetrics, GlyphSubstitutionCollection collection) { - if (this.Count == 0) - { - return this.Add(fontMetrics, collection); - } - bool hasFallBacks = false; List orphans = new(); - for (int i = start; i < Math.Min(end, this.offsets.Count); i++) + for (int i = 0; i < this.offsets.Count; i++) { int offset = this.offsets[i]; if (!collection.TryGetGlyphShapingDataAtOffset(offset, out GlyphShapingData? data)) @@ -117,7 +108,7 @@ public bool TryAddOrUpdate(FontMetrics fontMetrics, GlyphSubstitutionCollection } GlyphMetrics[] metrics = this.map[offset]; - if (!textRun && metrics[0].GlyphType != GlyphType.Fallback) + if (metrics[0].GlyphType != GlyphType.Fallback) { // We've already got the correct glyph. continue; @@ -166,7 +157,14 @@ public bool TryAddOrUpdate(FontMetrics fontMetrics, GlyphSubstitutionCollection return !hasFallBacks; } - private bool Add(FontMetrics fontMetrics, GlyphSubstitutionCollection collection) + /// + /// Adds the collection of glyph ids to the metrics collection. + /// identified as fallbacks. + /// + /// The font face with metrics. + /// The glyph substitution collection. + /// if the metrics collection does not contain any fallbacks; otherwise . + public bool TryAdd(FontMetrics fontMetrics, GlyphSubstitutionCollection collection) { bool hasFallBacks = false; for (int i = 0; i < collection.Count; i++) diff --git a/src/SixLabors.Fonts/TextLayout.cs b/src/SixLabors.Fonts/TextLayout.cs index 74bffe001..082a6f981 100644 --- a/src/SixLabors.Fonts/TextLayout.cs +++ b/src/SixLabors.Fonts/TextLayout.cs @@ -62,36 +62,17 @@ private static TextBox ProcessText(ReadOnlySpan text, TextOptions options) Dictionary bidiMap = new(); // Incrementally build out collection of glyphs. - // For each run we start with a fresh substitution collection to avoid - // overwriting the glyph ids. - IReadOnlyList textRuns = options.TextRuns ?? Array.Empty(); - int count = CodePoint.GetCodePointCount(text); - - // First do a complete run using our default font. - // This builds our position collection for overwriting. - bool complete = DoFontRun( - text, - 0, - count, - false, - mainFont, - bidiRuns, - bidiMap, - substitutions, - positionings); - - // Now overwrite with individual text runs. + IReadOnlyList textRuns = BuildTextRuns(text, options); + + // First do multiple font runs using the individual text runs. + bool complete = true; foreach (TextRun textRun in textRuns) { - FontMetrics runFont = textRun.Font?.FontMetrics ?? mainFont; - ReadOnlySpan runText = textRun.Slice(text); - substitutions.Clear(); if (!DoFontRun( - runText, + textRun.Slice(text), textRun.Start, - textRun.End, - true, - runFont, + false, + textRun.Font!.FontMetrics, bidiRuns, bidiMap, substitutions, @@ -104,14 +85,13 @@ private static TextBox ProcessText(ReadOnlySpan text, TextOptions options) if (!complete) { // Finally try our fallback fonts. + // We do a complete run here across the whole collection. foreach (FontMetrics font in fallbackFonts) { - substitutions.Clear(); if (DoFontRun( text, 0, - count, - false, + true, font, bidiRuns, bidiMap, @@ -126,12 +106,9 @@ private static TextBox ProcessText(ReadOnlySpan text, TextOptions options) // Update the positions of the glyphs in the completed collection. // Each set of metrics is associated with single font and will only be updated // by that font so it's safe to use a single collection. - mainFont.UpdatePositions(positionings); - foreach (TextRun textRun in textRuns) { - FontMetrics runFont = textRun.Font?.FontMetrics ?? mainFont; - runFont.UpdatePositions(positionings); + textRun.Font!.FontMetrics.UpdatePositions(positionings); } foreach (FontMetrics font in fallbackFonts) @@ -408,17 +385,75 @@ private static IEnumerable LayoutLineVertical( return glyphs; } + private static IReadOnlyList BuildTextRuns(ReadOnlySpan text, TextOptions options) + { + if (options.TextRuns?.Count == 0) + { + return new TextRun[] + { + new() + { + Start = 0, + End = CodePoint.GetCodePointCount(text), + Font = options.Font + } + }; + } + + int start = 0; + int end = CodePoint.GetCodePointCount(text); + List textRuns = new(); + foreach (TextRun textRun in options.TextRuns!.OrderBy(x => x.Start)) + { + // Fill gaps within runs. + if (textRun.Start > start) + { + textRuns.Add(new() + { + Start = start, + End = textRun.Start, + Font = options.Font + }); + } + + // Add the current run, ensuring the font is not null. + if (textRun.Font is null) + { + textRun.Font = options.Font; + } + + textRuns.Add(textRun); + start = textRun.End; + } + + // Add a final run. + if (start < end) + { + textRuns.Add(new() + { + Start = start, + End = end, + Font = options.Font + }); + } + + return textRuns; + } + private static bool DoFontRun( ReadOnlySpan text, int start, - int end, - bool textRun, + bool isFallbackRun, FontMetrics fontMetrics, BidiRun[] bidiRuns, Dictionary bidiMap, GlyphSubstitutionCollection substitutions, GlyphPositioningCollection positionings) { + // For each run we start with a fresh substitution collection to avoid + // overwriting the glyph ids. + substitutions.Clear(); + // Enumerate through each grapheme in the text. int graphemeIndex; int codePointIndex = start; @@ -472,7 +507,9 @@ private static bool DoFontRun( SubstituteBidiMirrors(fontMetrics, substitutions); fontMetrics.ApplySubstitution(substitutions); - return positionings.TryAddOrUpdate(fontMetrics, substitutions, start, end, textRun); + return !isFallbackRun + ? positionings.TryAdd(fontMetrics, substitutions) + : positionings.TryUpdate(fontMetrics, substitutions); } private static void SubstituteBidiMirrors(FontMetrics fontMetrics, GlyphSubstitutionCollection collection) From 65d838452965e79911fe1f8cc6be6bad7c82b855 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 3 Mar 2022 09:39:24 +1100 Subject: [PATCH 04/48] Ensure runs cannot overlap --- src/SixLabors.Fonts/TextLayout.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/SixLabors.Fonts/TextLayout.cs b/src/SixLabors.Fonts/TextLayout.cs index 082a6f981..af307ac5a 100644 --- a/src/SixLabors.Fonts/TextLayout.cs +++ b/src/SixLabors.Fonts/TextLayout.cs @@ -422,11 +422,15 @@ private static IReadOnlyList BuildTextRuns(ReadOnlySpan text, Tex textRun.Font = options.Font; } + // Ensure that the previous run does not overlap the current. + TextRun previous = textRuns[textRuns.Count - 1]; + previous.End = Math.Min(previous.End, start); + textRuns.Add(textRun); start = textRun.End; } - // Add a final run. + // Add a final run if required. if (start < end) { textRuns.Add(new() From 7b74864396be9bf7377eecb8bec6a49fa40ac673 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 3 Mar 2022 09:56:22 +1100 Subject: [PATCH 05/48] Validate run Start and End properties before slicing. --- src/SixLabors.Fonts/TextRun.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/SixLabors.Fonts/TextRun.cs b/src/SixLabors.Fonts/TextRun.cs index ed7227fb4..043eeb3cc 100644 --- a/src/SixLabors.Fonts/TextRun.cs +++ b/src/SixLabors.Fonts/TextRun.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System; +using System.Runtime.CompilerServices; using SixLabors.Fonts.Unicode; namespace SixLabors.Fonts @@ -38,6 +39,8 @@ public class TextRun /// The . public ReadOnlySpan Slice(ReadOnlySpan text) { + ValidateRange(this.Start, this.End); + // Convert code point indices into char indices so we can slice int chars = 0; int count = 0; @@ -65,5 +68,19 @@ public ReadOnlySpan Slice(ReadOnlySpan text) /// public override string ToString() => $"[TextRun: Start={this.Start}, End={this.End}, TextAttributes={this.TextAttributes}]"; + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void ValidateRange(int start, int end) + { + if (start < 0 || end < 0) + { + throw new ArgumentOutOfRangeException($"Start '{start}' and End '{end}' must be greater or equal to zero."); + } + + if (end <= start) + { + throw new ArgumentOutOfRangeException($"End '{end}' must be greater than Start '{start}'."); + } + } } } From a4a0e4d687fa9033931adeeb5a9c630970b035bb Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 3 Mar 2022 12:46:04 +1100 Subject: [PATCH 06/48] Add some TextRun tests, simplify TextLayout --- src/SixLabors.Fonts/TextLayout.cs | 128 +++++++++--------- src/SixLabors.Fonts/TextMeasurer.cs | 112 ++++----------- src/SixLabors.Fonts/TextRenderer.cs | 14 +- src/SixLabors.Fonts/TextRun.cs | 1 + tests/SixLabors.Fonts.Tests/GlyphTests.cs | 2 +- .../SixLabors.Fonts.Tests/Issues/Issues_47.cs | 8 +- .../SixLabors.Fonts.Tests/TextLayoutTests.cs | 91 ++++++++++++- tests/SixLabors.Fonts.Tests/TextRunTests.cs | 32 +++++ 8 files changed, 220 insertions(+), 168 deletions(-) create mode 100644 tests/SixLabors.Fonts.Tests/TextRunTests.cs diff --git a/src/SixLabors.Fonts/TextLayout.cs b/src/SixLabors.Fonts/TextLayout.cs index af307ac5a..a102152b7 100644 --- a/src/SixLabors.Fonts/TextLayout.cs +++ b/src/SixLabors.Fonts/TextLayout.cs @@ -14,11 +14,9 @@ namespace SixLabors.Fonts /// /// Encapsulated logic or laying out text. /// - internal class TextLayout + internal static class TextLayout { - internal static TextLayout Default { get; set; } = new(); - - public IReadOnlyList GenerateLayout(ReadOnlySpan text, TextOptions options) + public static IReadOnlyList GenerateLayout(ReadOnlySpan text, TextOptions options) { if (text.IsEmpty) { @@ -29,6 +27,69 @@ public IReadOnlyList GenerateLayout(ReadOnlySpan text, TextOp return LayoutText(textBox, options); } + public static IReadOnlyList BuildTextRuns(ReadOnlySpan text, TextOptions options) + { + if (options.TextRuns?.Count == 0) + { + return new TextRun[] + { + new() + { + Start = 0, + End = CodePoint.GetCodePointCount(text), + Font = options.Font + } + }; + } + + int start = 0; + int end = CodePoint.GetCodePointCount(text); + List textRuns = new(); + foreach (TextRun textRun in options.TextRuns!.OrderBy(x => x.Start)) + { + // Fill gaps within runs. + if (textRun.Start > start) + { + textRuns.Add(new() + { + Start = start, + End = textRun.Start, + Font = options.Font + }); + } + + // Add the current run, ensuring the font is not null. + if (textRun.Font is null) + { + textRun.Font = options.Font; + } + + // Ensure that the previous run does not overlap the current. + if (textRuns.Count > 0) + { + int prevIndex = textRuns.Count - 1; + TextRun previous = textRuns[prevIndex]; + previous.End = Math.Min(previous.End, textRun.Start); + } + + textRuns.Add(textRun); + start = textRun.End; + } + + // Add a final run if required. + if (start < end) + { + textRuns.Add(new() + { + Start = start, + End = end, + Font = options.Font + }); + } + + return textRuns; + } + private static TextBox ProcessText(ReadOnlySpan text, TextOptions options) { // Gather the font and fallbacks. @@ -385,65 +446,6 @@ private static IEnumerable LayoutLineVertical( return glyphs; } - private static IReadOnlyList BuildTextRuns(ReadOnlySpan text, TextOptions options) - { - if (options.TextRuns?.Count == 0) - { - return new TextRun[] - { - new() - { - Start = 0, - End = CodePoint.GetCodePointCount(text), - Font = options.Font - } - }; - } - - int start = 0; - int end = CodePoint.GetCodePointCount(text); - List textRuns = new(); - foreach (TextRun textRun in options.TextRuns!.OrderBy(x => x.Start)) - { - // Fill gaps within runs. - if (textRun.Start > start) - { - textRuns.Add(new() - { - Start = start, - End = textRun.Start, - Font = options.Font - }); - } - - // Add the current run, ensuring the font is not null. - if (textRun.Font is null) - { - textRun.Font = options.Font; - } - - // Ensure that the previous run does not overlap the current. - TextRun previous = textRuns[textRuns.Count - 1]; - previous.End = Math.Min(previous.End, start); - - textRuns.Add(textRun); - start = textRun.End; - } - - // Add a final run if required. - if (start < end) - { - textRuns.Add(new() - { - Start = start, - End = end, - Font = options.Font - }); - } - - return textRuns; - } - private static bool DoFontRun( ReadOnlySpan text, int start, diff --git a/src/SixLabors.Fonts/TextMeasurer.cs b/src/SixLabors.Fonts/TextMeasurer.cs index 417859176..aa74a4a78 100644 --- a/src/SixLabors.Fonts/TextMeasurer.cs +++ b/src/SixLabors.Fonts/TextMeasurer.cs @@ -9,7 +9,7 @@ namespace SixLabors.Fonts { /// - /// Encapsulated logic for laying out and measuring text. + /// Encapsulated logic for laying out and then measuring text properties. /// public static class TextMeasurer { @@ -17,65 +17,75 @@ public static class TextMeasurer /// Measures the text. /// /// The text. - /// The style. + /// The text shaping options. /// The size of the text if it was to be rendered. public static FontRectangle Measure(string text, TextOptions options) - => TextMeasurerInt.Default.Measure(text.AsSpan(), options); + => Measure(text.AsSpan(), options); /// /// Measures the text. /// /// The text. - /// The style. + /// The text shaping options. /// The size of the text if it was to be rendered. public static FontRectangle Measure(ReadOnlySpan text, TextOptions options) - => TextMeasurerInt.Default.Measure(text, options); + => GetSize(TextLayout.GenerateLayout(text, options), options.Dpi); /// /// Measures the text. /// /// The text. - /// The style. + /// The text shaping options. /// The size of the text if it was to be rendered. public static FontRectangle MeasureBounds(string text, TextOptions options) - => TextMeasurerInt.Default.MeasureBounds(text.AsSpan(), options); + => MeasureBounds(text.AsSpan(), options); /// /// Measures the text. /// /// The text. - /// The style. + /// The text shaping options. /// The size of the text if it was to be rendered. public static FontRectangle MeasureBounds(ReadOnlySpan text, TextOptions options) - => TextMeasurerInt.Default.MeasureBounds(text, options); + => GetBounds(TextLayout.GenerateLayout(text, options), options.Dpi); /// /// Measures the character bounds of the text. For each control character the list contains a null element. /// /// The text. - /// The style. + /// The text shaping options. + /// The list of character bounds of the text if it was to be rendered. + /// Whether any of the characters had non-empty bounds. + public static bool TryMeasureCharacterBounds(string text, TextOptions options, out GlyphBounds[] characterBounds) + => TryMeasureCharacterBounds(text.AsSpan(), options, out characterBounds); + + /// + /// Measures the character bounds of the text. For each control character the list contains a null element. + /// + /// The text. + /// The text shaping options. /// The list of character bounds of the text if it was to be rendered. /// Whether any of the characters had non-empty bounds. public static bool TryMeasureCharacterBounds(ReadOnlySpan text, TextOptions options, out GlyphBounds[] characterBounds) - => TextMeasurerInt.Default.TryMeasureCharacterBounds(text, options, out characterBounds); + => TryGetCharacterBounds(TextLayout.GenerateLayout(text, options), options.Dpi, out characterBounds); /// /// Gets the number of lines contained within the text. /// /// The text. - /// The style. + /// The text shaping options. /// The line count. public static int CountLines(string text, TextOptions options) - => TextMeasurerInt.Default.CountLines(text.AsSpan(), options); + => CountLines(text.AsSpan(), options); /// /// Gets the number of lines contained within the text. /// /// The text. - /// The style. + /// The text shaping options. /// The line count. public static int CountLines(ReadOnlySpan text, TextOptions options) - => TextMeasurerInt.Default.CountLines(text, options); + => TextLayout.GenerateLayout(text, options).Count(x => x.IsStartOfLine); internal static FontRectangle GetSize(IReadOnlyList glyphLayouts, float dpi) { @@ -156,77 +166,5 @@ internal static bool TryGetCharacterBounds(IReadOnlyList glyphLayou characterBounds = characterBoundsList; return hasSize; } - - internal class TextMeasurerInt - { - private readonly TextLayout layoutEngine; - - internal TextMeasurerInt(TextLayout layoutEngine) - => this.layoutEngine = layoutEngine; - - /// - /// Initializes a new instance of the class. - /// - internal TextMeasurerInt() - : this(TextLayout.Default) - { - } - - internal static TextMeasurerInt Default { get; set; } = new TextMeasurerInt(); - - /// - /// Measures the text. - /// - /// The text. - /// The style. - /// The size of the text if it was to be rendered. - internal FontRectangle MeasureBounds(ReadOnlySpan text, TextOptions options) - { - IReadOnlyList glyphsToRender = this.layoutEngine.GenerateLayout(text, options); - - return GetBounds(glyphsToRender, options.Dpi); - } - - /// - /// Measures the text. - /// - /// The text. - /// The style. - /// The character bounds list. - /// The size of the text if it was to be rendered. - internal bool TryMeasureCharacterBounds(ReadOnlySpan text, TextOptions options, out GlyphBounds[] characterBounds) - { - IReadOnlyList glyphsToRender = this.layoutEngine.GenerateLayout(text, options); - - return TryGetCharacterBounds(glyphsToRender, options.Dpi, out characterBounds); - } - - /// - /// Measures the text. - /// - /// The text. - /// The style. - /// The size of the text if it was to be rendered. - internal FontRectangle Measure(ReadOnlySpan text, TextOptions options) - { - IReadOnlyList glyphsToRender = this.layoutEngine.GenerateLayout(text, options); - - return GetSize(glyphsToRender, options.Dpi); - } - - /// - /// Gets the number of lines contained within the text. - /// - /// The text. - /// The style. - /// The line count. - internal int CountLines(ReadOnlySpan text, TextOptions options) - { - IReadOnlyList glyphsToRender = this.layoutEngine.GenerateLayout(text, options); - int usedLines = glyphsToRender.Count(x => x.IsStartOfLine); - - return usedLines; - } - } } } diff --git a/src/SixLabors.Fonts/TextRenderer.cs b/src/SixLabors.Fonts/TextRenderer.cs index fb4da3178..66328211d 100644 --- a/src/SixLabors.Fonts/TextRenderer.cs +++ b/src/SixLabors.Fonts/TextRenderer.cs @@ -11,23 +11,13 @@ namespace SixLabors.Fonts /// public class TextRenderer { - private readonly TextLayout layoutEngine; private readonly IGlyphRenderer renderer; - internal TextRenderer(IGlyphRenderer renderer, TextLayout layoutEngine) - { - this.layoutEngine = layoutEngine; - this.renderer = renderer; - } - /// /// Initializes a new instance of the class. /// /// The renderer. - public TextRenderer(IGlyphRenderer renderer) - : this(renderer, TextLayout.Default) - { - } + public TextRenderer(IGlyphRenderer renderer) => this.renderer = renderer; /// /// Renders the text to the . @@ -62,7 +52,7 @@ public void RenderText(string text, TextOptions options) /// The style. public void RenderText(ReadOnlySpan text, TextOptions options) { - IReadOnlyList glyphsToRender = this.layoutEngine.GenerateLayout(text, options); + IReadOnlyList glyphsToRender = TextLayout.GenerateLayout(text, options); FontRectangle rect = TextMeasurer.GetBounds(glyphsToRender, options.Dpi); this.renderer.BeginText(rect); diff --git a/src/SixLabors.Fonts/TextRun.cs b/src/SixLabors.Fonts/TextRun.cs index 043eeb3cc..7b07e0d71 100644 --- a/src/SixLabors.Fonts/TextRun.cs +++ b/src/SixLabors.Fonts/TextRun.cs @@ -37,6 +37,7 @@ public class TextRun /// /// The text to slice. /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] public ReadOnlySpan Slice(ReadOnlySpan text) { ValidateRange(this.Start, this.End); diff --git a/tests/SixLabors.Fonts.Tests/GlyphTests.cs b/tests/SixLabors.Fonts.Tests/GlyphTests.cs index 7f9e8a0bb..1b842e20c 100644 --- a/tests/SixLabors.Fonts.Tests/GlyphTests.cs +++ b/tests/SixLabors.Fonts.Tests/GlyphTests.cs @@ -133,7 +133,7 @@ public void RenderColrGlyphWithVariationSelector() Font font = new FontCollection().Add(TestFonts.TwemojiMozillaData()).CreateFont(12); string text = "\u263A\uFE0F"; // Fully-qualified sequence for emoji 'smiling face' - IReadOnlyList layout = new TextLayout().GenerateLayout(text.AsSpan(), new TextOptions(font)); + IReadOnlyList layout = TextLayout.GenerateLayout(text.AsSpan(), new TextOptions(font)); // Check that no glyphs were generated by the variation selector Assert.True(layout.All(v => v.Glyph.GlyphMetrics.CodePoint.Value == 0x263A)); diff --git a/tests/SixLabors.Fonts.Tests/Issues/Issues_47.cs b/tests/SixLabors.Fonts.Tests/Issues/Issues_47.cs index 6749e36f6..7f3b5b981 100644 --- a/tests/SixLabors.Fonts.Tests/Issues/Issues_47.cs +++ b/tests/SixLabors.Fonts.Tests/Issues/Issues_47.cs @@ -19,7 +19,7 @@ public void LeftAlignedTextNewLineShouldNotStartWithWhiteSpace(string text) var r = new GlyphRenderer(); - IReadOnlyList layout = new TextLayout().GenerateLayout(text.AsSpan(), new TextOptions(new Font(font, 30)) + IReadOnlyList layout = TextLayout.GenerateLayout(text.AsSpan(), new TextOptions(new Font(font, 30)) { WrappingLength = 350, HorizontalAlignment = HorizontalAlignment.Left @@ -47,7 +47,7 @@ public void NewWrappedLinesShouldNotStartOrEndWithWhiteSpace(string text, Horizo var r = new GlyphRenderer(); - IReadOnlyList layout = new TextLayout().GenerateLayout(text.AsSpan(), new TextOptions(new Font(font, 30)) + IReadOnlyList layout = TextLayout.GenerateLayout(text.AsSpan(), new TextOptions(new Font(font, 30)) { WrappingLength = 350, HorizontalAlignment = horizontalAlignment @@ -74,7 +74,7 @@ public void WhiteSpaceAtStartOfTextShouldNotBeTrimmed() var r = new GlyphRenderer(); - IReadOnlyList layout = new TextLayout().GenerateLayout(text.AsSpan(), new TextOptions(new Font(font, 30)) + IReadOnlyList layout = TextLayout.GenerateLayout(text.AsSpan(), new TextOptions(new Font(font, 30)) { WrappingLength = 350 }); @@ -92,7 +92,7 @@ public void WhiteSpaceAtTheEndOfTextShouldBeTrimmed() var r = new GlyphRenderer(); - IReadOnlyList layout = new TextLayout().GenerateLayout(text.AsSpan(), new TextOptions(new Font(font, 30)) + IReadOnlyList layout = TextLayout.GenerateLayout(text.AsSpan(), new TextOptions(new Font(font, 30)) { WrappingLength = 350 }); diff --git a/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs b/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs index 00c318caa..2093c7e4b 100644 --- a/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs +++ b/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs @@ -84,7 +84,7 @@ public void VerticalAlignmentTests( VerticalAlignment = vertical }; - IReadOnlyList glyphsToRender = new TextLayout().GenerateLayout(text.AsSpan(), span); + IReadOnlyList glyphsToRender = TextLayout.GenerateLayout(text.AsSpan(), span); FontRectangle bound = TextMeasurer.GetBounds(glyphsToRender, span.Dpi); Assert.Equal(310, bound.Width, 3); @@ -379,6 +379,95 @@ public void CountLinesWrappingLength(string text, int wrappingLength, int usedLi Assert.Equal(usedLines, count); } + [Fact] + public void BuildTextRuns_EmptyReturnsDefaultRun() + { + const string text = "This is a long and Honorificabilitudinitatibus califragilisticexpialidocious"; + Font font = CreateFont(text); + TextOptions options = new(font); + + IReadOnlyList runs = TextLayout.BuildTextRuns(text.AsSpan(), options); + + Assert.True(runs.Count == 1); + Assert.Equal(font, runs[0].Font); + Assert.Equal(0, runs[0].Start); + Assert.Equal(CodePoint.GetCodePointCount(text.AsSpan()), runs[0].End); + } + + [Fact] + public void BuildTextRuns_ReturnsCreatesInterimRuns() + { + const string text = "This is a long and Honorificabilitudinitatibus califragilisticexpialidocious"; + Font font = CreateFont(text); + TextOptions options = new(font) + { + TextRuns = new List() + { + new TextRun() { Start = 9, End = 23 }, + new TextRun() { Start = 35, End = 54 }, + new TextRun() { Start = 68, End = 70 }, + } + }; + + IReadOnlyList runs = TextLayout.BuildTextRuns(text.AsSpan(), options); + + Assert.True(runs.Count == 7); + + Assert.Equal(0, runs[0].Start); + Assert.Equal(9, runs[0].End); + Assert.Equal(9, runs[0].Slice(text.AsSpan()).Length); + + Assert.Equal(9, runs[1].Start); + Assert.Equal(23, runs[1].End); + Assert.Equal(14, runs[1].Slice(text.AsSpan()).Length); + + Assert.Equal(23, runs[2].Start); + Assert.Equal(35, runs[2].End); + Assert.Equal(12, runs[2].Slice(text.AsSpan()).Length); + + Assert.Equal(35, runs[3].Start); + Assert.Equal(54, runs[3].End); + Assert.Equal(19, runs[3].Slice(text.AsSpan()).Length); + + Assert.Equal(54, runs[4].Start); + Assert.Equal(68, runs[4].End); + Assert.Equal(14, runs[4].Slice(text.AsSpan()).Length); + + Assert.Equal(68, runs[5].Start); + Assert.Equal(70, runs[5].End); + Assert.Equal(2, runs[5].Slice(text.AsSpan()).Length); + + Assert.Equal(70, runs[6].Start); + Assert.Equal(76, runs[6].End); + Assert.Equal(6, runs[6].Slice(text.AsSpan()).Length); + } + + [Fact] + public void BuildTextRuns_PreventsOverlappingRun() + { + const string text = "This is a long and Honorificabilitudinitatibus califragilisticexpialidocious"; + Font font = CreateFont(text); + TextOptions options = new(font) + { + TextRuns = new List() + { + new TextRun() { Start = 0, End = 23 }, + new TextRun() { Start = 1, End = 76 }, + } + }; + + IReadOnlyList runs = TextLayout.BuildTextRuns(text.AsSpan(), options); + + Assert.Equal(2, runs.Count); + Assert.Equal(font, runs[0].Font); + Assert.Equal(0, runs[0].Start); + Assert.Equal(1, runs[0].End); + + Assert.Equal(font, runs[1].Font); + Assert.Equal(1, runs[1].Start); + Assert.Equal(76, runs[1].End); + } + public static Font CreateFont(string text) { var fc = (IFontMetricsCollection)new FontCollection(); diff --git a/tests/SixLabors.Fonts.Tests/TextRunTests.cs b/tests/SixLabors.Fonts.Tests/TextRunTests.cs new file mode 100644 index 000000000..37d0c257e --- /dev/null +++ b/tests/SixLabors.Fonts.Tests/TextRunTests.cs @@ -0,0 +1,32 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using Xunit; + +namespace SixLabors.Fonts.Tests +{ + public class TextRunTests + { + private const string Text = "This is a long and Honorificabilitudinitatibus califragilisticexpialidocious"; + + [Theory] + [InlineData(-1, -1)] + [InlineData(0, 0)] + [InlineData(1, 0)] + [InlineData(1, 1)] + [InlineData(3, 2)] + public void ThrowsZeroValue(int start, int end) + { + Exception ex = Record.Exception(() => new TextRun() { Start = start, End = end }.Slice(Text.AsSpan())); + Assert.NotNull(ex); + } + + [Fact] + public void SlicesTextCorrectly() + { + ReadOnlySpan slice = new TextRun() { Start = 19, End = 46 }.Slice(Text.AsSpan()); + Assert.True("Honorificabilitudinitatibus".AsSpan().SequenceEqual(slice)); + } + } +} From 37962900fac4af4c89e3f033cd6951cbb185d524 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 3 Mar 2022 12:50:16 +1100 Subject: [PATCH 07/48] Assert Font is set --- .../SixLabors.Fonts.Tests/TextLayoutTests.cs | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs b/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs index 2093c7e4b..1ceb6b544 100644 --- a/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs +++ b/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs @@ -399,13 +399,14 @@ public void BuildTextRuns_ReturnsCreatesInterimRuns() { const string text = "This is a long and Honorificabilitudinitatibus califragilisticexpialidocious"; Font font = CreateFont(text); + Font font2 = CreateFont(text, 16); TextOptions options = new(font) { TextRuns = new List() { - new TextRun() { Start = 9, End = 23 }, - new TextRun() { Start = 35, End = 54 }, - new TextRun() { Start = 68, End = 70 }, + new TextRun() { Start = 9, End = 23, Font = font2 }, + new TextRun() { Start = 35, End = 54, Font = font2 }, + new TextRun() { Start = 68, End = 70, Font = font2 }, } }; @@ -415,30 +416,37 @@ public void BuildTextRuns_ReturnsCreatesInterimRuns() Assert.Equal(0, runs[0].Start); Assert.Equal(9, runs[0].End); + Assert.Equal(font, runs[0].Font); Assert.Equal(9, runs[0].Slice(text.AsSpan()).Length); Assert.Equal(9, runs[1].Start); Assert.Equal(23, runs[1].End); + Assert.Equal(font2, runs[1].Font); Assert.Equal(14, runs[1].Slice(text.AsSpan()).Length); Assert.Equal(23, runs[2].Start); Assert.Equal(35, runs[2].End); + Assert.Equal(font, runs[2].Font); Assert.Equal(12, runs[2].Slice(text.AsSpan()).Length); Assert.Equal(35, runs[3].Start); Assert.Equal(54, runs[3].End); + Assert.Equal(font2, runs[3].Font); Assert.Equal(19, runs[3].Slice(text.AsSpan()).Length); Assert.Equal(54, runs[4].Start); Assert.Equal(68, runs[4].End); + Assert.Equal(font, runs[4].Font); Assert.Equal(14, runs[4].Slice(text.AsSpan()).Length); Assert.Equal(68, runs[5].Start); Assert.Equal(70, runs[5].End); + Assert.Equal(font2, runs[5].Font); Assert.Equal(2, runs[5].Slice(text.AsSpan()).Length); Assert.Equal(70, runs[6].Start); Assert.Equal(76, runs[6].End); + Assert.Equal(font, runs[6].Font); Assert.Equal(6, runs[6].Slice(text.AsSpan()).Length); } @@ -474,5 +482,12 @@ public static Font CreateFont(string text) Font d = fc.AddMetrics(new FakeFontInstance(text), CultureInfo.InvariantCulture).CreateFont(12); return new Font(d, 1); } + + public static Font CreateFont(string text, float pointSize) + { + var fc = (IFontMetricsCollection)new FontCollection(); + Font d = fc.AddMetrics(new FakeFontInstance(text), CultureInfo.InvariantCulture).CreateFont(12); + return new Font(d, pointSize); + } } } From e2b7bd5082df3f4dc346680392cd13b15ca6fc37 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 3 Mar 2022 23:37:36 +1100 Subject: [PATCH 08/48] Persist TextRun font sizes --- .../GlyphPositioningCollection.cs | 58 ++++++++++++++----- src/SixLabors.Fonts/TextLayout.cs | 30 +++++----- 2 files changed, 56 insertions(+), 32 deletions(-) diff --git a/src/SixLabors.Fonts/GlyphPositioningCollection.cs b/src/SixLabors.Fonts/GlyphPositioningCollection.cs index 151ee94c0..b69f1532f 100644 --- a/src/SixLabors.Fonts/GlyphPositioningCollection.cs +++ b/src/SixLabors.Fonts/GlyphPositioningCollection.cs @@ -28,7 +28,7 @@ internal sealed class GlyphPositioningCollection : IGlyphShapingCollection /// /// Contains a map between non-sequential codepoint offsets and their glyphs. /// - private readonly Dictionary map = new(); + private readonly Dictionary map = new(); /// /// Initializes a new instance of the class. @@ -76,24 +76,37 @@ public void EnableShapingFeature(int index, Tag feature) /// Gets the glyph metrics at the given codepoint offset. /// /// The zero-based index within the input codepoint collection. + /// The font size in PT units of the font containing this glyph. /// /// When this method returns, contains the glyph metrics associated with the specified offset, /// if the value is found; otherwise, the default value for the type of the metrics parameter. /// This parameter is passed uninitialized. /// - /// The . - public bool TryGetGlyphMetricsAtOffset(int offset, [NotNullWhen(true)] out GlyphMetrics[]? metrics) - => this.map.TryGetValue(offset, out metrics); + /// The metrics. + public bool TryGetGlyphMetricsAtOffset(int offset, out float pointSize, [NotNullWhen(true)] out GlyphMetrics[]? metrics) + { + if (this.map.TryGetValue(offset, out PointSizeMetricsPair? entry)) + { + pointSize = entry.PointSize; + metrics = entry.Metrics; + return true; + } + + pointSize = 0; + metrics = null; + return false; + } /// /// Updates the collection of glyph ids to the metrics collection to overwrite any glyphs that have been previously /// identified as fallbacks. /// - /// The font face with metrics. + /// The font face with metrics. /// The glyph substitution collection. /// if the metrics collection does not contain any fallbacks; otherwise . - public bool TryUpdate(FontMetrics fontMetrics, GlyphSubstitutionCollection collection) + public bool TryUpdate(Font font, GlyphSubstitutionCollection collection) { + FontMetrics? fontMetrics = font.FontMetrics; bool hasFallBacks = false; List orphans = new(); for (int i = 0; i < this.offsets.Count; i++) @@ -107,8 +120,8 @@ public bool TryUpdate(FontMetrics fontMetrics, GlyphSubstitutionCollection colle continue; } - GlyphMetrics[] metrics = this.map[offset]; - if (metrics[0].GlyphType != GlyphType.Fallback) + PointSizeMetricsPair pair = this.map[offset]; + if (pair.Metrics[0].GlyphType != GlyphType.Fallback) { // We've already got the correct glyph. continue; @@ -139,7 +152,7 @@ public bool TryUpdate(FontMetrics fontMetrics, GlyphSubstitutionCollection colle if (m.Count > 0) { GlyphMetrics[] gm = m.ToArray(); - this.map[offset] = gm; + this.map[offset] = new(pair.PointSize, gm); this.glyphs[i] = new(data, true) { Bounds = new(0, 0, gm[0].AdvanceWidth, gm[0].AdvanceHeight) }; this.offsets[i] = offset; } @@ -161,10 +174,10 @@ public bool TryUpdate(FontMetrics fontMetrics, GlyphSubstitutionCollection colle /// Adds the collection of glyph ids to the metrics collection. /// identified as fallbacks. /// - /// The font face with metrics. + /// The font face with metrics. /// The glyph substitution collection. /// if the metrics collection does not contain any fallbacks; otherwise . - public bool TryAdd(FontMetrics fontMetrics, GlyphSubstitutionCollection collection) + public bool TryAdd(Font font, GlyphSubstitutionCollection collection) { bool hasFallBacks = false; for (int i = 0; i < collection.Count; i++) @@ -178,7 +191,7 @@ public bool TryAdd(FontMetrics fontMetrics, GlyphSubstitutionCollection collecti { // Perform a semi-deep clone (FontMetrics is not cloned) so we can continue to // cache the original in the font metrics and only update our collection. - foreach (GlyphMetrics gm in fontMetrics.GetGlyphMetrics(codePoint, id, this.TextOptions.ColorFontSupport)) + foreach (GlyphMetrics gm in font.FontMetrics.GetGlyphMetrics(codePoint, id, this.TextOptions.ColorFontSupport)) { if (gm.GlyphType == GlyphType.Fallback && !CodePoint.IsControl(codePoint)) { @@ -192,7 +205,7 @@ public bool TryAdd(FontMetrics fontMetrics, GlyphSubstitutionCollection collecti if (m.Count > 0) { GlyphMetrics[] gm = m.ToArray(); - this.map[offset] = gm; + this.map[offset] = new(font.Size, gm); if (this.IsVerticalLayoutMode) { this.glyphs.Add(new(data, true) { Bounds = new(0, 0, 0, gm[0].AdvanceHeight) }); @@ -223,7 +236,7 @@ public void UpdatePosition(FontMetrics fontMetrics, ushort index) } ushort glyphId = data.GlyphIds[0]; - foreach (GlyphMetrics m in this.map[this.offsets[index]]) + foreach (GlyphMetrics m in this.map[this.offsets[index]].Metrics) { if (m.GlyphId == glyphId && fontMetrics == m.FontMetrics) { @@ -245,7 +258,7 @@ public void UpdatePosition(FontMetrics fontMetrics, ushort index) /// The delta y-advance. public void Advance(FontMetrics fontMetrics, ushort index, ushort glyphId, short dx, short dy) { - foreach (GlyphMetrics m in this.map[this.offsets[index]]) + foreach (GlyphMetrics m in this.map[this.offsets[index]].Metrics) { if (m.GlyphId == glyphId && fontMetrics == m.FontMetrics) { @@ -261,6 +274,19 @@ public void Advance(FontMetrics fontMetrics, ushort index, ushort glyphId, short /// The zero-based index of the elements to offset. /// if the element should be processed; otherwise, . public bool ShouldProcess(FontMetrics fontMetrics, ushort index) - => this.map[this.offsets[index]][0].FontMetrics == fontMetrics; + => this.map[this.offsets[index]].Metrics[0].FontMetrics == fontMetrics; + + private class PointSizeMetricsPair + { + public PointSizeMetricsPair(float pointSize, GlyphMetrics[] metrics) + { + this.PointSize = pointSize; + this.Metrics = metrics; + } + + public float PointSize { get; set; } + + public GlyphMetrics[] Metrics { get; set; } + } } } diff --git a/src/SixLabors.Fonts/TextLayout.cs b/src/SixLabors.Fonts/TextLayout.cs index a102152b7..619fef82a 100644 --- a/src/SixLabors.Fonts/TextLayout.cs +++ b/src/SixLabors.Fonts/TextLayout.cs @@ -93,10 +93,9 @@ public static IReadOnlyList BuildTextRuns(ReadOnlySpan text, Text private static TextBox ProcessText(ReadOnlySpan text, TextOptions options) { // Gather the font and fallbacks. - FontMetrics mainFont = options.Font.FontMetrics; - FontMetrics[] fallbackFonts = (options.FallbackFontFamilies?.Count > 0) - ? options.FallbackFontFamilies.Select(x => new Font(x, options.Font.Size, options.Font.RequestedStyle).FontMetrics).ToArray() - : Array.Empty(); + Font[] fallbackFonts = (options.FallbackFontFamilies?.Count > 0) + ? options.FallbackFontFamilies.Select(x => new Font(x, options.Font.Size, options.Font.RequestedStyle)).ToArray() + : Array.Empty(); LayoutMode layoutMode = options.LayoutMode; GlyphSubstitutionCollection substitutions = new(options); @@ -133,7 +132,7 @@ private static TextBox ProcessText(ReadOnlySpan text, TextOptions options) textRun.Slice(text), textRun.Start, false, - textRun.Font!.FontMetrics, + textRun.Font!, bidiRuns, bidiMap, substitutions, @@ -147,7 +146,7 @@ private static TextBox ProcessText(ReadOnlySpan text, TextOptions options) { // Finally try our fallback fonts. // We do a complete run here across the whole collection. - foreach (FontMetrics font in fallbackFonts) + foreach (Font font in fallbackFonts) { if (DoFontRun( text, @@ -172,9 +171,9 @@ private static TextBox ProcessText(ReadOnlySpan text, TextOptions options) textRun.Font!.FontMetrics.UpdatePositions(positionings); } - foreach (FontMetrics font in fallbackFonts) + foreach (Font font in fallbackFonts) { - font.UpdatePositions(positionings); + font.FontMetrics.UpdatePositions(positionings); } return BreakLines(text, options, bidiRuns, bidiMap, positionings, layoutMode); @@ -450,7 +449,7 @@ private static bool DoFontRun( ReadOnlySpan text, int start, bool isFallbackRun, - FontMetrics fontMetrics, + Font font, BidiRun[] bidiRuns, Dictionary bidiMap, GlyphSubstitutionCollection substitutions, @@ -500,7 +499,7 @@ private static bool DoFontRun( charIndex += charsConsumed; // Get the glyph id for the codepoint and add to the collection. - fontMetrics.TryGetGlyphId(current, next, out ushort glyphId, out skipNextCodePoint); + font.FontMetrics.TryGetGlyphId(current, next, out ushort glyphId, out skipNextCodePoint); substitutions.AddGlyph(glyphId, current, (TextDirection)bidiRuns[bidiRun].Direction, codePointIndex); codePointIndex++; @@ -510,12 +509,12 @@ private static bool DoFontRun( // Apply the simple and complex substitutions. // TODO: Investigate HarfBuzz normlizer. - SubstituteBidiMirrors(fontMetrics, substitutions); - fontMetrics.ApplySubstitution(substitutions); + SubstituteBidiMirrors(font.FontMetrics, substitutions); + font.FontMetrics.ApplySubstitution(substitutions); return !isFallbackRun - ? positionings.TryAdd(fontMetrics, substitutions) - : positionings.TryUpdate(fontMetrics, substitutions); + ? positionings.TryAdd(font, substitutions) + : positionings.TryUpdate(font, substitutions); } private static void SubstituteBidiMirrors(FontMetrics fontMetrics, GlyphSubstitutionCollection collection) @@ -568,7 +567,6 @@ private static TextBox BreakLines( GlyphPositioningCollection positionings, LayoutMode layoutMode) { - float pointSize = options.Font.Size; bool shouldWrap = options.WrappingLength > 0; float wrappingLength = shouldWrap ? options.WrappingLength / options.Dpi : float.MaxValue; bool breakAll = options.WordBreaking == WordBreaking.BreakAll; @@ -607,7 +605,7 @@ private static TextBox BreakLines( var codePointEnumerator = new SpanCodePointEnumerator(graphemeEnumerator.Current); while (codePointEnumerator.MoveNext()) { - if (!positionings.TryGetGlyphMetricsAtOffset(codePointIndex, out GlyphMetrics[]? metrics)) + if (!positionings.TryGetGlyphMetricsAtOffset(codePointIndex, out float pointSize, out GlyphMetrics[]? metrics)) { // Codepoint was skipped during original enumeration. codePointIndex++; From 3b62bab6f4d30f49a9dc72971f06c6913879c75b Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 4 Mar 2022 12:37:07 +1100 Subject: [PATCH 09/48] Do groundwork for attribute support. --- src/SixLabors.Fonts/FileFontMetrics.cs | 38 +++++- src/SixLabors.Fonts/Font.cs | 5 +- src/SixLabors.Fonts/FontMetrics.cs | 56 ++++++++- src/SixLabors.Fonts/GlyphMetrics.cs | 8 ++ .../GlyphPositioningCollection.cs | 11 +- src/SixLabors.Fonts/GlyphShapingData.cs | 6 + .../GlyphSubstitutionCollection.cs | 4 +- src/SixLabors.Fonts/StreamFontMetrics.cs | 58 +++++++-- .../AdvancedTypographic/GPos/AnchorTable.cs | 2 +- .../Tables/General/OS2Table.cs | 113 +++++++++++++----- src/SixLabors.Fonts/TextLayout.cs | 11 +- 11 files changed, 258 insertions(+), 54 deletions(-) diff --git a/src/SixLabors.Fonts/FileFontMetrics.cs b/src/SixLabors.Fonts/FileFontMetrics.cs index 6e9f5770a..e09101906 100644 --- a/src/SixLabors.Fonts/FileFontMetrics.cs +++ b/src/SixLabors.Fonts/FileFontMetrics.cs @@ -70,6 +70,36 @@ internal FileFontMetrics(FontDescription description, string path, long offset) /// public override short AdvanceHeightMax => this.metrics.Value.AdvanceHeightMax; + /// + public override short SubscriptXSize => this.metrics.Value.SubscriptXSize; + + /// + public override short SubscriptYSize => this.metrics.Value.SubscriptYSize; + + /// + public override short SubscriptXOffset => this.metrics.Value.SubscriptXOffset; + + /// + public override short SubscriptYOffset => this.metrics.Value.SubscriptYOffset; + + /// + public override short SuperscriptXSize => this.metrics.Value.SuperscriptXSize; + + /// + public override short SuperscriptYSize => this.metrics.Value.SuperscriptYSize; + + /// + public override short SuperscriptXOffset => this.metrics.Value.SuperscriptXOffset; + + /// + public override short SuperscriptYOffset => this.metrics.Value.SuperscriptYOffset; + + /// + public override short StrikeoutSize => this.metrics.Value.StrikeoutSize; + + /// + public override short StrikeoutPosition => this.metrics.Value.StrikeoutPosition; + /// internal override bool TryGetGlyphId(CodePoint codePoint, out ushort glyphId) => this.metrics.Value.TryGetGlyphId(codePoint, out glyphId); @@ -91,12 +121,12 @@ internal override bool TryGetMarkAttachmentClass(ushort glyphId, [NotNullWhen(tr => this.metrics.Value.TryGetMarkAttachmentClass(glyphId, out markAttachmentClass); /// - public override IEnumerable GetGlyphMetrics(CodePoint codePoint, ColorFontSupport support) - => this.metrics.Value.GetGlyphMetrics(codePoint, support); + public override IEnumerable GetGlyphMetrics(CodePoint codePoint, TextAttribute textAttributes, ColorFontSupport support) + => this.metrics.Value.GetGlyphMetrics(codePoint, textAttributes, support); /// - internal override IEnumerable GetGlyphMetrics(CodePoint codePoint, ushort glyphId, ColorFontSupport support) - => this.metrics.Value.GetGlyphMetrics(codePoint, glyphId, support); + internal override IEnumerable GetGlyphMetrics(CodePoint codePoint, ushort glyphId, TextAttribute textAttributes, ColorFontSupport support) + => this.metrics.Value.GetGlyphMetrics(codePoint, glyphId, textAttributes, support); /// internal override void ApplySubstitution(GlyphSubstitutionCollection collection) diff --git a/src/SixLabors.Fonts/Font.cs b/src/SixLabors.Fonts/Font.cs index 0c0387fe0..6b118047b 100644 --- a/src/SixLabors.Fonts/Font.cs +++ b/src/SixLabors.Fonts/Font.cs @@ -146,11 +146,12 @@ public bool TryGetPath([NotNullWhen(true)] out string? path) /// Gets the glyphs for the given codepoint. /// /// The code point of the character. + /// The text attributes to assign to the glyph. /// Options for enabling color font support during layout and rendering. /// Returns the glyph - public IEnumerable GetGlyphs(CodePoint codePoint, ColorFontSupport support) + public IEnumerable GetGlyphs(CodePoint codePoint, TextAttribute textAttributes, ColorFontSupport support) { - foreach (GlyphMetrics metrics in this.FontMetrics.GetGlyphMetrics(codePoint, support)) + foreach (GlyphMetrics metrics in this.FontMetrics.GetGlyphMetrics(codePoint, textAttributes, support)) { yield return new(metrics, this.Size); } diff --git a/src/SixLabors.Fonts/FontMetrics.cs b/src/SixLabors.Fonts/FontMetrics.cs index f6d7913ab..616e80ce5 100644 --- a/src/SixLabors.Fonts/FontMetrics.cs +++ b/src/SixLabors.Fonts/FontMetrics.cs @@ -67,6 +67,56 @@ internal FontMetrics() /// public abstract short AdvanceHeightMax { get; } + /// + /// Gets the recommended horizontal size in font design units for subscripts for this font. + /// + public abstract short SubscriptXSize { get; } + + /// + /// Gets the recommended vertical size in font design units for subscripts for this font. + /// + public abstract short SubscriptYSize { get; } + + /// + /// Gets the recommended horizontal offset in font design units for subscripts for this font. + /// + public abstract short SubscriptXOffset { get; } + + /// + /// Gets the recommended vertical offset in font design units for subscripts for this font. + /// + public abstract short SubscriptYOffset { get; } + + /// + /// Gets the recommended horizontal size in font design units for superscripts for this font. + /// + public abstract short SuperscriptXSize { get; } + + /// + /// Gets the recommended vertical size in font design units for superscripts for this font. + /// + public abstract short SuperscriptYSize { get; } + + /// + /// Gets the recommended horizontal offset in font design units for superscripts for this font. + /// + public abstract short SuperscriptXOffset { get; } + + /// + /// Gets the recommended vertical offset in font design units for superscripts for this font. + /// + public abstract short SuperscriptYOffset { get; } + + /// + /// Gets thickness of the strikeout stroke in font design units. + /// + public abstract short StrikeoutSize { get; } + + /// + /// Gets the position of the top of the strikeout stroke relative to the baseline in font design units. + /// + public abstract short StrikeoutPosition { get; } + /// /// Gets the specified glyph id matching the codepoint. /// @@ -121,9 +171,10 @@ internal FontMetrics() /// Gets the glyph metrics for a given code point. /// /// The Unicode code point to get the glyph for. + /// The text attributes to assign to the glyph. /// Options for enabling color font support during layout and rendering. /// The glyph metrics to find. - public abstract IEnumerable GetGlyphMetrics(CodePoint codePoint, ColorFontSupport support); + public abstract IEnumerable GetGlyphMetrics(CodePoint codePoint, TextAttribute textAttributes, ColorFontSupport support); /// /// Gets the glyph metrics for a given code point and glyph id. @@ -133,9 +184,10 @@ internal FontMetrics() /// The previously matched or substituted glyph id for the codepoint in the face. /// If this value equals 0 the default fallback metrics are returned. /// + /// The text attributes to assigned to the glyph. /// Options for enabling color font support during layout and rendering. /// The . - internal abstract IEnumerable GetGlyphMetrics(CodePoint codePoint, ushort glyphId, ColorFontSupport support); + internal abstract IEnumerable GetGlyphMetrics(CodePoint codePoint, ushort glyphId, TextAttribute textAttributes, ColorFontSupport support); /// /// Applies any available substitutions to the collection of glyphs. diff --git a/src/SixLabors.Fonts/GlyphMetrics.cs b/src/SixLabors.Fonts/GlyphMetrics.cs index 7d00ec3db..65e7e5a5a 100644 --- a/src/SixLabors.Fonts/GlyphMetrics.cs +++ b/src/SixLabors.Fonts/GlyphMetrics.cs @@ -29,6 +29,7 @@ internal GlyphMetrics( ushort unitsPerEM, ushort glyphId, GlyphType glyphType = GlyphType.Standard, + TextAttribute textAttributes = TextAttribute.None, GlyphColor? glyphColor = null) { this.FontMetrics = font; @@ -44,6 +45,7 @@ internal GlyphMetrics( this.Width = bounds.Max.X - bounds.Min.X; this.Height = bounds.Max.Y - bounds.Min.Y; this.GlyphType = glyphType; + this.TextAttributes = textAttributes; this.LeftSideBearing = leftSideBearing; this.TopSideBearing = topSideBearing; this.ScaleFactor = this.UnitsPerEm * 72F; @@ -64,6 +66,7 @@ internal GlyphMetrics(GlyphMetrics other, CodePoint codePoint) this.Width = other.Width; this.Height = other.Height; this.GlyphType = other.GlyphType; + this.TextAttributes = other.TextAttributes; this.LeftSideBearing = other.LeftSideBearing; this.TopSideBearing = other.TopSideBearing; this.ScaleFactor = other.ScaleFactor; @@ -115,6 +118,11 @@ internal GlyphMetrics(GlyphMetrics other, CodePoint codePoint) /// public GlyphType GlyphType { get; } + /// + /// Gets the text attributes applied to this glyph. + /// + public TextAttribute TextAttributes { get; } + /// /// Gets the color of this glyph when the is /// diff --git a/src/SixLabors.Fonts/GlyphPositioningCollection.cs b/src/SixLabors.Fonts/GlyphPositioningCollection.cs index b69f1532f..519c74cb9 100644 --- a/src/SixLabors.Fonts/GlyphPositioningCollection.cs +++ b/src/SixLabors.Fonts/GlyphPositioningCollection.cs @@ -106,7 +106,8 @@ public bool TryGetGlyphMetricsAtOffset(int offset, out float pointSize, [NotNull /// if the metrics collection does not contain any fallbacks; otherwise . public bool TryUpdate(Font font, GlyphSubstitutionCollection collection) { - FontMetrics? fontMetrics = font.FontMetrics; + FontMetrics fontMetrics = font.FontMetrics; + ColorFontSupport colorFontSupport = this.TextOptions.ColorFontSupport; bool hasFallBacks = false; List orphans = new(); for (int i = 0; i < this.offsets.Count; i++) @@ -129,13 +130,14 @@ public bool TryUpdate(Font font, GlyphSubstitutionCollection collection) CodePoint codePoint = data.CodePoint; ushort[] glyphIds = data.GlyphIds; + TextAttribute textAttributes = data.TextAttributes; var m = new List(glyphIds.Length); foreach (ushort id in glyphIds) { // Perform a semi-deep clone (FontMetrics is not cloned) so we can continue to // cache the original in the font metrics and only update our collection. - foreach (GlyphMetrics gm in fontMetrics.GetGlyphMetrics(codePoint, id, this.TextOptions.ColorFontSupport)) + foreach (GlyphMetrics gm in fontMetrics.GetGlyphMetrics(codePoint, id, textAttributes, colorFontSupport)) { if (gm.GlyphType == GlyphType.Fallback && !CodePoint.IsControl(codePoint)) { @@ -180,18 +182,21 @@ public bool TryUpdate(Font font, GlyphSubstitutionCollection collection) public bool TryAdd(Font font, GlyphSubstitutionCollection collection) { bool hasFallBacks = false; + FontMetrics fontMetrics = font.FontMetrics; + ColorFontSupport colorFontSupport = this.TextOptions.ColorFontSupport; for (int i = 0; i < collection.Count; i++) { GlyphShapingData data = collection.GetGlyphShapingData(i, out int offset); CodePoint codePoint = data.CodePoint; ushort[] glyphIds = data.GlyphIds; + TextAttribute textAttributes = data.TextAttributes; var m = new List(glyphIds.Length); foreach (ushort id in glyphIds) { // Perform a semi-deep clone (FontMetrics is not cloned) so we can continue to // cache the original in the font metrics and only update our collection. - foreach (GlyphMetrics gm in font.FontMetrics.GetGlyphMetrics(codePoint, id, this.TextOptions.ColorFontSupport)) + foreach (GlyphMetrics gm in fontMetrics.GetGlyphMetrics(codePoint, id, textAttributes, colorFontSupport)) { if (gm.GlyphType == GlyphType.Fallback && !CodePoint.IsControl(codePoint)) { diff --git a/src/SixLabors.Fonts/GlyphShapingData.cs b/src/SixLabors.Fonts/GlyphShapingData.cs index a2b46bb6d..0616ab880 100644 --- a/src/SixLabors.Fonts/GlyphShapingData.cs +++ b/src/SixLabors.Fonts/GlyphShapingData.cs @@ -32,6 +32,7 @@ public GlyphShapingData(GlyphShapingData data, bool clearFeatures = false) this.CodePoint = data.CodePoint; this.CodePointCount = data.CodePointCount; this.Direction = data.Direction; + this.TextAttributes = data.TextAttributes; this.GlyphIds = data.GlyphIds; this.LigatureId = data.LigatureId; this.LigatureComponent = data.LigatureComponent; @@ -61,6 +62,11 @@ public GlyphShapingData(GlyphShapingData data, bool clearFeatures = false) /// public TextDirection Direction { get; set; } + /// + /// Gets or sets the text attributes applied to this glyph. + /// + public TextAttribute TextAttributes { get; set; } + /// /// Gets or sets the collection of glyph ids. /// diff --git a/src/SixLabors.Fonts/GlyphSubstitutionCollection.cs b/src/SixLabors.Fonts/GlyphSubstitutionCollection.cs index 6fe5c8292..8eee37081 100644 --- a/src/SixLabors.Fonts/GlyphSubstitutionCollection.cs +++ b/src/SixLabors.Fonts/GlyphSubstitutionCollection.cs @@ -94,13 +94,15 @@ public void EnableShapingFeature(int index, Tag feature) /// The id of the glyph to add. /// The codepoint the glyph represents. /// The resolved text direction for the codepoint. + /// The text attributes to apply to the glyph. /// The zero-based index within the input codepoint collection. - public void AddGlyph(ushort glyphId, CodePoint codePoint, TextDirection direction, int offset) + public void AddGlyph(ushort glyphId, CodePoint codePoint, TextDirection direction, TextAttribute textAttributes, int offset) { this.glyphs.Add(offset, new() { CodePoint = codePoint, Direction = direction, + TextAttributes = textAttributes, GlyphIds = new[] { glyphId }, }); this.offsets.Add(offset); diff --git a/src/SixLabors.Fonts/StreamFontMetrics.cs b/src/SixLabors.Fonts/StreamFontMetrics.cs index bc8363ad5..114de0328 100644 --- a/src/SixLabors.Fonts/StreamFontMetrics.cs +++ b/src/SixLabors.Fonts/StreamFontMetrics.cs @@ -152,6 +152,17 @@ internal StreamFontMetrics( this.AdvanceWidthMax = (short)horizontalHeadTable.AdvanceWidthMax; this.AdvanceHeightMax = verticalHeadTable == null ? this.LineHeight : verticalHeadTable.AdvanceHeightMax; + this.SubscriptXSize = os2.SubscriptXSize; + this.SubscriptYSize = os2.SubscriptYSize; + this.SubscriptXOffset = os2.SubscriptXOffset; + this.SubscriptYOffset = os2.SubscriptYOffset; + this.SuperscriptXSize = os2.SuperscriptXSize; + this.SuperscriptYSize = os2.SuperscriptYSize; + this.SuperscriptXOffset = os2.SuperscriptXOffset; + this.SuperscriptYOffset = os2.SuperscriptYOffset; + this.StrikeoutSize = os2.StrikeoutSize; + this.StrikeoutPosition = os2.StrikeoutPosition; + this.kerningTable = kern; this.gSubTable = gSubTable; this.gPosTable = gPosTable; @@ -191,6 +202,36 @@ internal StreamFontMetrics( /// public override short AdvanceHeightMax { get; } + /// + public override short SubscriptXSize { get; } + + /// + public override short SubscriptYSize { get; } + + /// + public override short SubscriptXOffset { get; } + + /// + public override short SubscriptYOffset { get; } + + /// + public override short SuperscriptXSize { get; } + + /// + public override short SuperscriptYSize { get; } + + /// + public override short SuperscriptXOffset { get; } + + /// + public override short SuperscriptYOffset { get; } + + /// + public override short StrikeoutSize { get; } + + /// + public override short StrikeoutPosition { get; } + /// internal override bool TryGetGlyphId(CodePoint codePoint, out ushort glyphId) => this.TryGetGlyphId(codePoint, null, out glyphId, out bool _); @@ -224,14 +265,14 @@ internal override bool TryGetMarkAttachmentClass(ushort glyphId, [NotNullWhen(tr } /// - public override IEnumerable GetGlyphMetrics(CodePoint codePoint, ColorFontSupport support) + public override IEnumerable GetGlyphMetrics(CodePoint codePoint, TextAttribute textAttributes, ColorFontSupport support) { this.TryGetGlyphId(codePoint, out ushort glyphId); - return this.GetGlyphMetrics(codePoint, glyphId, support); + return this.GetGlyphMetrics(codePoint, glyphId, textAttributes, support); } /// - internal override IEnumerable GetGlyphMetrics(CodePoint codePoint, ushort glyphId, ColorFontSupport support) + internal override IEnumerable GetGlyphMetrics(CodePoint codePoint, ushort glyphId, TextAttribute textAttributes, ColorFontSupport support) { GlyphType glyphType = GlyphType.Standard; if (glyphId == 0) @@ -242,7 +283,7 @@ internal override IEnumerable GetGlyphMetrics(CodePoint codePoint, } if (support == ColorFontSupport.MicrosoftColrFormat - && this.TryGetColoredVectors(codePoint, glyphId, out GlyphMetrics[]? metrics)) + && this.TryGetColoredVectors(codePoint, glyphId, textAttributes, out GlyphMetrics[]? metrics)) { return metrics; } @@ -254,7 +295,8 @@ internal override IEnumerable GetGlyphMetrics(CodePoint codePoint, this.CreateGlyphMetrics( codePoint, glyphId, - glyphType) + glyphType, + textAttributes) }; } @@ -469,7 +511,7 @@ internal GlyphVector ApplyHinting(GlyphVector glyphVector, float pixelSize, usho return glyphVector; } - private bool TryGetColoredVectors(CodePoint codePoint, ushort glyphId, [NotNullWhen(true)] out GlyphMetrics[]? vectors) + private bool TryGetColoredVectors(CodePoint codePoint, ushort glyphId, TextAttribute textAttributes, [NotNullWhen(true)] out GlyphMetrics[]? vectors) { if (this.colrTable == null || this.colorGlyphCache == null) { @@ -488,7 +530,7 @@ private bool TryGetColoredVectors(CodePoint codePoint, ushort glyphId, [NotNullW { LayerRecord? layer = indexes[i]; - vectors[i] = this.CreateGlyphMetrics(codePoint, layer.GlyphId, GlyphType.ColrLayer, layer.PaletteIndex); + vectors[i] = this.CreateGlyphMetrics(codePoint, layer.GlyphId, GlyphType.ColrLayer, textAttributes, layer.PaletteIndex); } } @@ -503,6 +545,7 @@ private GlyphMetrics CreateGlyphMetrics( CodePoint codePoint, ushort glyphId, GlyphType glyphType, + TextAttribute textAttributes, ushort palleteIndex = 0) { GlyphVector vector = this.glyphs.GetGlyph(glyphId); @@ -540,6 +583,7 @@ private GlyphMetrics CreateGlyphMetrics( this.UnitsPerEm, glyphId, glyphType, + textAttributes, color); } } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/AnchorTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/AnchorTable.cs index ca14fdf22..cc19761b7 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/AnchorTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/AnchorTable.cs @@ -111,7 +111,7 @@ public override AnchorXY GetAnchor(FontMetrics fontMetrics, GlyphShapingData dat { if (collection.TextOptions.ApplyHinting) { - foreach (GlyphMetrics metric in fontMetrics.GetGlyphMetrics(data.CodePoint, collection.TextOptions.ColorFontSupport)) + foreach (GlyphMetrics metric in fontMetrics.GetGlyphMetrics(data.CodePoint, data.TextAttributes, collection.TextOptions.ColorFontSupport)) { ReadOnlyMemory points = metric.GetOutline().ControlPoints; if (this.anchorPointIndex < points.Length) diff --git a/src/SixLabors.Fonts/Tables/General/OS2Table.cs b/src/SixLabors.Fonts/Tables/General/OS2Table.cs index 2bc8c55f3..b69df790c 100644 --- a/src/SixLabors.Fonts/Tables/General/OS2Table.cs +++ b/src/SixLabors.Fonts/Tables/General/OS2Table.cs @@ -30,33 +30,52 @@ internal sealed class OS2Table : Table private readonly ushort weightClass; private readonly ushort widthClass; private readonly short averageCharWidth; - private readonly short strikeoutPosition; - private readonly short strikeoutSize; - private readonly short subscriptXOffset; - private readonly short subscriptXSize; - private readonly short subscriptYOffset; - private readonly short subscriptYSize; - private readonly short superscriptXOffset; - private readonly short superscriptXSize; - private readonly short superscriptYOffset; - private readonly short superscriptYSize; - - public OS2Table(short averageCharWidth, ushort weightClass, ushort widthClass, ushort styleType, short subscriptXSize, short subscriptYSize, short subscriptXOffset, short subscriptYOffset, short superscriptXSize, short superscriptYSize, short superscriptXOffset, short superscriptYOffset, short strikeoutSize, short strikeoutPosition, short familyClass, byte[] panose, uint unicodeRange1, uint unicodeRange2, uint unicodeRange3, uint unicodeRange4, string tag, FontStyleSelection fontStyle, ushort firstCharIndex, ushort lastCharIndex, short typoAscender, short typoDescender, short typoLineGap, ushort winAscent, ushort winDescent) + + public OS2Table( + short averageCharWidth, + ushort weightClass, + ushort widthClass, + ushort styleType, + short subscriptXSize, + short subscriptYSize, + short subscriptXOffset, + short subscriptYOffset, + short superscriptXSize, + short superscriptYSize, + short superscriptXOffset, + short superscriptYOffset, + short strikeoutSize, + short strikeoutPosition, + short familyClass, + byte[] panose, + uint unicodeRange1, + uint unicodeRange2, + uint unicodeRange3, + uint unicodeRange4, + string tag, + FontStyleSelection fontStyle, + ushort firstCharIndex, + ushort lastCharIndex, + short typoAscender, + short typoDescender, + short typoLineGap, + ushort winAscent, + ushort winDescent) { this.averageCharWidth = averageCharWidth; this.weightClass = weightClass; this.widthClass = widthClass; this.styleType = styleType; - this.subscriptXSize = subscriptXSize; - this.subscriptYSize = subscriptYSize; - this.subscriptXOffset = subscriptXOffset; - this.subscriptYOffset = subscriptYOffset; - this.superscriptXSize = superscriptXSize; - this.superscriptYSize = superscriptYSize; - this.superscriptXOffset = superscriptXOffset; - this.superscriptYOffset = superscriptYOffset; - this.strikeoutSize = strikeoutSize; - this.strikeoutPosition = strikeoutPosition; + this.SubscriptXSize = subscriptXSize; + this.SubscriptYSize = subscriptYSize; + this.SubscriptXOffset = subscriptXOffset; + this.SubscriptYOffset = subscriptYOffset; + this.SuperscriptXSize = superscriptXSize; + this.SuperscriptYSize = superscriptYSize; + this.SuperscriptXOffset = superscriptXOffset; + this.SuperscriptYOffset = superscriptYOffset; + this.StrikeoutSize = strikeoutSize; + this.StrikeoutPosition = strikeoutPosition; this.familyClass = familyClass; this.panose = panose; this.unicodeRange1 = unicodeRange1; @@ -74,22 +93,30 @@ public OS2Table(short averageCharWidth, ushort weightClass, ushort widthClass, u this.WinDescent = winDescent; } - public OS2Table(OS2Table version0Table, ushort codePageRange1, ushort codePageRange2, short heightX, short capHeight, ushort defaultChar, ushort breakChar, ushort maxContext) + public OS2Table( + OS2Table version0Table, + ushort codePageRange1, + ushort codePageRange2, + short heightX, + short capHeight, + ushort defaultChar, + ushort breakChar, + ushort maxContext) : this( version0Table.averageCharWidth, version0Table.weightClass, version0Table.widthClass, version0Table.styleType, - version0Table.subscriptXSize, - version0Table.subscriptYSize, - version0Table.subscriptXOffset, - version0Table.subscriptYOffset, - version0Table.superscriptXSize, - version0Table.superscriptYSize, - version0Table.superscriptXOffset, - version0Table.superscriptYOffset, - version0Table.strikeoutSize, - version0Table.strikeoutPosition, + version0Table.SubscriptXSize, + version0Table.SubscriptYSize, + version0Table.SubscriptXOffset, + version0Table.SubscriptYOffset, + version0Table.SuperscriptXSize, + version0Table.SuperscriptYSize, + version0Table.SuperscriptXOffset, + version0Table.SuperscriptYOffset, + version0Table.StrikeoutSize, + version0Table.StrikeoutPosition, version0Table.familyClass, version0Table.panose, version0Table.unicodeRange1, @@ -177,6 +204,26 @@ internal enum FontStyleSelection : ushort public ushort WinDescent { get; } + public short StrikeoutPosition { get; } + + public short StrikeoutSize { get; } + + public short SubscriptXOffset { get; } + + public short SubscriptXSize { get; } + + public short SubscriptYOffset { get; } + + public short SubscriptYSize { get; } + + public short SuperscriptXOffset { get; } + + public short SuperscriptXSize { get; } + + public short SuperscriptYOffset { get; } + + public short SuperscriptYSize { get; } + public static OS2Table? Load(FontReader fontReader) { if (!fontReader.TryGetReaderAtTablePosition(TableName, out BigEndianBinaryReader? binaryReader)) diff --git a/src/SixLabors.Fonts/TextLayout.cs b/src/SixLabors.Fonts/TextLayout.cs index 619fef82a..ed6713eb4 100644 --- a/src/SixLabors.Fonts/TextLayout.cs +++ b/src/SixLabors.Fonts/TextLayout.cs @@ -131,6 +131,7 @@ private static TextBox ProcessText(ReadOnlySpan text, TextOptions options) if (!DoFontRun( textRun.Slice(text), textRun.Start, + textRuns, false, textRun.Font!, bidiRuns, @@ -151,6 +152,7 @@ private static TextBox ProcessText(ReadOnlySpan text, TextOptions options) if (DoFontRun( text, 0, + textRuns, true, font, bidiRuns, @@ -448,6 +450,7 @@ private static IEnumerable LayoutLineVertical( private static bool DoFontRun( ReadOnlySpan text, int start, + IReadOnlyList textRuns, bool isFallbackRun, Font font, BidiRun[] bidiRuns, @@ -463,6 +466,7 @@ private static bool DoFontRun( int graphemeIndex; int codePointIndex = start; int bidiRun = 0; + int textRun = 0; var graphemeEnumerator = new SpanGraphemeEnumerator(text); for (graphemeIndex = 0; graphemeEnumerator.MoveNext(); graphemeIndex++) { @@ -480,6 +484,11 @@ private static bool DoFontRun( bidiRun++; } + if (codePointIndex == textRuns[textRun].End) + { + textRun++; + } + if (skipNextCodePoint) { codePointIndex++; @@ -500,7 +509,7 @@ private static bool DoFontRun( // Get the glyph id for the codepoint and add to the collection. font.FontMetrics.TryGetGlyphId(current, next, out ushort glyphId, out skipNextCodePoint); - substitutions.AddGlyph(glyphId, current, (TextDirection)bidiRuns[bidiRun].Direction, codePointIndex); + substitutions.AddGlyph(glyphId, current, (TextDirection)bidiRuns[bidiRun].Direction, textRuns[textRun].TextAttributes, codePointIndex); codePointIndex++; graphemeCodePointIndex++; From 9283316bbb675771249c2b32101e3cc4e86314af Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 4 Mar 2022 12:48:49 +1100 Subject: [PATCH 10/48] Fix tests build --- tests/SixLabors.Fonts.Tests/FontLoaderTests.cs | 12 ++++++------ tests/SixLabors.Fonts.Tests/FontMetricsTests.cs | 16 ++++++++-------- tests/SixLabors.Fonts.Tests/GlyphTests.cs | 4 ++-- tests/SixLabors.Fonts.Tests/Issues/Issues_191.cs | 5 +++-- tests/SixLabors.Fonts.Tests/TextLayoutTests.cs | 2 +- 5 files changed, 20 insertions(+), 19 deletions(-) diff --git a/tests/SixLabors.Fonts.Tests/FontLoaderTests.cs b/tests/SixLabors.Fonts.Tests/FontLoaderTests.cs index 6f126bb0f..031915044 100644 --- a/tests/SixLabors.Fonts.Tests/FontLoaderTests.cs +++ b/tests/SixLabors.Fonts.Tests/FontLoaderTests.cs @@ -13,7 +13,7 @@ public class FontLoaderTests public void Issue21_LoopDetectedLoadingGlyphs() { Font font = new FontCollection().Add(TestFonts.CarterOneFileData()).CreateFont(12); - font.FontMetrics.GetGlyphMetrics(new CodePoint('\0'), ColorFontSupport.None).First(); + font.FontMetrics.GetGlyphMetrics(new CodePoint('\0'), TextAttribute.None, ColorFontSupport.None).First(); } [Fact] @@ -39,7 +39,7 @@ public void LoadFont_WithTtfFormat() { Font font = new FontCollection().Add(TestFonts.OpenSansFile).CreateFont(12); - GlyphMetrics glyph = font.FontMetrics.GetGlyphMetrics(new CodePoint('A'), ColorFontSupport.None).First(); + GlyphMetrics glyph = font.FontMetrics.GetGlyphMetrics(new CodePoint('A'), TextAttribute.None, ColorFontSupport.None).First(); var r = new GlyphRenderer(); glyph.RenderTo(r, font.Size, System.Numerics.Vector2.Zero, new TextOptions(font)); @@ -53,7 +53,7 @@ public void LoadFont_WithWoff1Format() { Font font = new FontCollection().Add(TestFonts.OpenSansFileWoff1).CreateFont(12); - GlyphMetrics glyph = font.FontMetrics.GetGlyphMetrics(new CodePoint('A'), ColorFontSupport.None).First(); + GlyphMetrics glyph = font.FontMetrics.GetGlyphMetrics(new CodePoint('A'), TextAttribute.None, ColorFontSupport.None).First(); var r = new GlyphRenderer(); glyph.RenderTo(r, font.Size, System.Numerics.Vector2.Zero, new TextOptions(font)); @@ -86,7 +86,7 @@ public void LoadFont_WithWoff2Format() { Font font = new FontCollection().Add(TestFonts.OpensSansWoff2Data()).CreateFont(12); - GlyphMetrics glyph = font.FontMetrics.GetGlyphMetrics(new CodePoint('A'), ColorFontSupport.None).First(); + GlyphMetrics glyph = font.FontMetrics.GetGlyphMetrics(new CodePoint('A'), TextAttribute.None, ColorFontSupport.None).First(); var r = new GlyphRenderer(); glyph.RenderTo(r, font.Size, System.Numerics.Vector2.Zero, new TextOptions(font)); @@ -104,7 +104,7 @@ public void LoadFont() Assert.Equal("SixLaborsSampleAB regular", font.FontMetrics.Description.FontNameInvariantCulture); Assert.Equal("Regular", font.FontMetrics.Description.FontSubFamilyNameInvariantCulture); - GlyphMetrics glyph = font.FontMetrics.GetGlyphMetrics(new CodePoint('a'), ColorFontSupport.None).First(); + GlyphMetrics glyph = font.FontMetrics.GetGlyphMetrics(new CodePoint('a'), TextAttribute.None, ColorFontSupport.None).First(); var r = new GlyphRenderer(); glyph.RenderTo(r, font.Size, System.Numerics.Vector2.Zero, new TextOptions(font)); @@ -120,7 +120,7 @@ public void LoadFontWoff() Assert.Equal("SixLaborsSampleAB regular", font.FontMetrics.Description.FontNameInvariantCulture); Assert.Equal("Regular", font.FontMetrics.Description.FontSubFamilyNameInvariantCulture); - GlyphMetrics glyph = font.FontMetrics.GetGlyphMetrics(new CodePoint('a'), ColorFontSupport.None).First(); + GlyphMetrics glyph = font.FontMetrics.GetGlyphMetrics(new CodePoint('a'), TextAttribute.None, ColorFontSupport.None).First(); var r = new GlyphRenderer(); glyph.RenderTo(r, 12, System.Numerics.Vector2.Zero, new TextOptions(font)); diff --git a/tests/SixLabors.Fonts.Tests/FontMetricsTests.cs b/tests/SixLabors.Fonts.Tests/FontMetricsTests.cs index 0af0e926c..eae58bd02 100644 --- a/tests/SixLabors.Fonts.Tests/FontMetricsTests.cs +++ b/tests/SixLabors.Fonts.Tests/FontMetricsTests.cs @@ -59,8 +59,8 @@ public void GlyphMetricsMatchesReference() Font font = family.CreateFont(12); var codePoint = new CodePoint('A'); - GlyphMetrics glyphMetrics = font.FontMetrics.GetGlyphMetrics(codePoint, ColorFontSupport.None).First(); - GlyphMetrics glyphMetrics1 = font.GetGlyphs(codePoint, ColorFontSupport.None).First().GlyphMetrics; + GlyphMetrics glyphMetrics = font.FontMetrics.GetGlyphMetrics(codePoint, TextAttribute.None, ColorFontSupport.None).First(); + GlyphMetrics glyphMetrics1 = font.GetGlyphs(codePoint, TextAttribute.None, ColorFontSupport.None).First().GlyphMetrics; Assert.Equal(glyphMetrics, glyphMetrics1); @@ -86,8 +86,8 @@ public void GlyphMetricsMatchesReference_WithWoff1format() Font font = family.CreateFont(12); var codePoint = new CodePoint('A'); - GlyphMetrics glyphMetrics = font.FontMetrics.GetGlyphMetrics(codePoint, ColorFontSupport.None).First(); - GlyphMetrics glyphMetrics1 = font.GetGlyphs(codePoint, ColorFontSupport.None).First().GlyphMetrics; + GlyphMetrics glyphMetrics = font.FontMetrics.GetGlyphMetrics(codePoint, TextAttribute.None, ColorFontSupport.None).First(); + GlyphMetrics glyphMetrics1 = font.GetGlyphs(codePoint, TextAttribute.None, ColorFontSupport.None).First().GlyphMetrics; Assert.Equal(glyphMetrics, glyphMetrics1); @@ -114,8 +114,8 @@ public void GlyphMetricsMatchesReference_WithWoff2format() Font font = family.CreateFont(12); var codePoint = new CodePoint('A'); - GlyphMetrics glyphMetrics = font.FontMetrics.GetGlyphMetrics(codePoint, ColorFontSupport.None).First(); - GlyphMetrics glyphMetrics1 = font.GetGlyphs(codePoint, ColorFontSupport.None).First().GlyphMetrics; + GlyphMetrics glyphMetrics = font.FontMetrics.GetGlyphMetrics(codePoint, TextAttribute.None, ColorFontSupport.None).First(); + GlyphMetrics glyphMetrics1 = font.GetGlyphs(codePoint, TextAttribute.None, ColorFontSupport.None).First().GlyphMetrics; Assert.Equal(glyphMetrics, glyphMetrics1); @@ -142,8 +142,8 @@ public void GlyphMetricsVerticalMatchesReference() Font font = family.CreateFont(12); var codePoint = new CodePoint('A'); - GlyphMetrics glyphMetrics = font.FontMetrics.GetGlyphMetrics(codePoint, ColorFontSupport.None).First(); - GlyphMetrics glyphMetrics1 = font.GetGlyphs(codePoint, ColorFontSupport.None).First().GlyphMetrics; + GlyphMetrics glyphMetrics = font.FontMetrics.GetGlyphMetrics(codePoint, TextAttribute.None, ColorFontSupport.None).First(); + GlyphMetrics glyphMetrics1 = font.GetGlyphs(codePoint, TextAttribute.None, ColorFontSupport.None).First().GlyphMetrics; Assert.Equal(glyphMetrics, glyphMetrics1); diff --git a/tests/SixLabors.Fonts.Tests/GlyphTests.cs b/tests/SixLabors.Fonts.Tests/GlyphTests.cs index 1b842e20c..9442ba969 100644 --- a/tests/SixLabors.Fonts.Tests/GlyphTests.cs +++ b/tests/SixLabors.Fonts.Tests/GlyphTests.cs @@ -93,7 +93,7 @@ public void LoadGlyph() Font font = new FontCollection().Add(TestFonts.SimpleFontFileData()).CreateFont(12); // Get letter A - Glyph g = font.GetGlyphs(new CodePoint(41), ColorFontSupport.None).First(); + Glyph g = font.GetGlyphs(new CodePoint(41), TextAttribute.None, ColorFontSupport.None).First(); GlyphOutline instance = g.GlyphMetrics.GetOutline(); Assert.Equal(20, instance.ControlPoints.Length); @@ -109,7 +109,7 @@ public void RenderColrGlyph() var instance = font.FontMetrics as StreamFontMetrics; CodePoint codePoint = this.AsCodePoint("😀"); Assert.True(instance.TryGetGlyphId(codePoint, out ushort idx)); - IEnumerable vectors = instance.GetGlyphMetrics(codePoint, idx, ColorFontSupport.MicrosoftColrFormat); + IEnumerable vectors = instance.GetGlyphMetrics(codePoint, idx, TextAttribute.None, ColorFontSupport.MicrosoftColrFormat); Assert.Equal(3, vectors.Count()); } diff --git a/tests/SixLabors.Fonts.Tests/Issues/Issues_191.cs b/tests/SixLabors.Fonts.Tests/Issues/Issues_191.cs index 3dbe7d64c..8a86249dc 100644 --- a/tests/SixLabors.Fonts.Tests/Issues/Issues_191.cs +++ b/tests/SixLabors.Fonts.Tests/Issues/Issues_191.cs @@ -17,9 +17,10 @@ public void CanLoadMacintoshGlyphs() .First(x => x.GetAvailableStyles().Contains(FontStyle.Regular)).CreateFont(12); const ColorFontSupport support = ColorFontSupport.None; + const TextAttribute attributes = TextAttribute.None; - Glyph[] a = font.GetGlyphs(new CodePoint('A'), support).ToArray(); - Glyph[] x = font.GetGlyphs(new CodePoint('x'), support).ToArray(); + Glyph[] a = font.GetGlyphs(new CodePoint('A'), attributes, support).ToArray(); + Glyph[] x = font.GetGlyphs(new CodePoint('x'), attributes, support).ToArray(); Glyph ga = Assert.Single(a); Glyph gx = Assert.Single(x); diff --git a/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs b/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs index 1ceb6b544..428b8a2eb 100644 --- a/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs +++ b/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs @@ -18,7 +18,7 @@ public class TextLayoutTests public void FakeFontGetGlyph() { Font font = CreateFont("hello world"); - Glyph glyph = font.GetGlyphs(new CodePoint('h'), ColorFontSupport.None).First(); + Glyph glyph = font.GetGlyphs(new CodePoint('h'), TextAttribute.None, ColorFontSupport.None).First(); Assert.NotEqual(default, glyph); } From ac767a89111c4fb42e06e5caefa1948ba56931b8 Mon Sep 17 00:00:00 2001 From: redth Date: Thu, 3 Mar 2022 21:19:56 -0500 Subject: [PATCH 11/48] First pass at POST table Roughly correct and ported from another project I had this in before, but not exactly tested after porting, so may be some rough edges --- .../Tables/General/Post/PostRecord.cs | 18 ++ .../Tables/General/PostTable.cs | 178 ++++++++++++++++++ .../BigEndianBinaryWriter.cs | 18 ++ .../SixLabors.Fonts.Tests/WriterExtensions.cs | 48 +++++ 4 files changed, 262 insertions(+) create mode 100644 src/SixLabors.Fonts/Tables/General/Post/PostRecord.cs create mode 100644 src/SixLabors.Fonts/Tables/General/PostTable.cs diff --git a/src/SixLabors.Fonts/Tables/General/Post/PostRecord.cs b/src/SixLabors.Fonts/Tables/General/Post/PostRecord.cs new file mode 100644 index 000000000..2c0f112c7 --- /dev/null +++ b/src/SixLabors.Fonts/Tables/General/Post/PostRecord.cs @@ -0,0 +1,18 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.Fonts.Tables.General.Post +{ + internal class PostNameRecord + { + internal PostNameRecord(ushort nameIndex, string name) + { + this.Name = name; + this.NameIndex = nameIndex; + } + + public ushort NameIndex { get; } + + public string Name { get; } + } +} diff --git a/src/SixLabors.Fonts/Tables/General/PostTable.cs b/src/SixLabors.Fonts/Tables/General/PostTable.cs new file mode 100644 index 000000000..c6bb6ced3 --- /dev/null +++ b/src/SixLabors.Fonts/Tables/General/PostTable.cs @@ -0,0 +1,178 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using SixLabors.Fonts.Tables.General.Post; + +namespace SixLabors.Fonts.Tables.General +{ + [TableName(TableName)] + internal class PostTable : Table + { + internal const string TableName = "POST"; + + private static readonly string[] AppleGlyphNameMap = new[] { ".notdef", ".null", "nonmarkingreturn", "space", "exclam", "quotedbl", "numbersign", "dollar", "percent", "ampersand", "quotesingle", "parenleft", "parenright", "asterisk", "plus", "comma", "hyphen", "period", "slash", "zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "colon", "semicolon", "less", "equal", "greater", "question", "at", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "bracketleft", "backslash", "bracketright", "asciicircum", "underscore", "grave", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "braceleft", "bar", "braceright", "asciitilde", "Adieresis", "Aring", "Ccedilla", "Eacute", "Ntilde", "Odieresis", "Udieresis", "aacute", "agrave", "acircumflex", "adieresis", "atilde", "aring", "ccedilla", "eacute", "egrave", "ecircumflex", "edieresis", "iacute", "igrave", "icircumflex", "idieresis", "ntilde", "oacute", "ograve", "ocircumflex", "odieresis", "otilde", "uacute", "ugrave", "ucircumflex", "udieresis", "dagger", "degree", "cent", "sterling", "section", "bullet", "paragraph", "germandbls", "registered", "copyright", "trademark", "acute", "dieresis", "notequal", "AE", "Oslash", "infinity", "plusminus", "lessequal", "greaterequal", "yen", "mu", "partialdiff", "summation", "product", "pi", "integral", "ordfeminine", "ordmasculine", "Omega", "ae", "oslash", "questiondown", "exclamdown", "logicalnot", "radical", "florin", "approxequal", "Delta", "guillemotleft", "guillemotright", "ellipsis", "nonbreakingspace", "Agrave", "Atilde", "Otilde", "OE", "oe", "endash", "emdash", "quotedblleft", "quotedblright", "quoteleft", "quoteright", "divide", "lozenge", "ydieresis", "Ydieresis", "fraction", "currency", "guilsinglleft", "guilsinglright", "fi", "fl", "daggerdbl", "periodcentered", "quotesinglbase", "quotedblbase", "perthousand", "Acircumflex", "Ecircumflex", "Aacute", "Edieresis", "Egrave", "Iacute", "Icircumflex", "Idieresis", "Igrave", "Oacute", "Ocircumflex", "apple", "Ograve", "Uacute", "Ucircumflex", "Ugrave", "dotlessi", "circumflex", "tilde", "macron", "breve", "dotaccent", "ring", "cedilla", "hungarumlaut", "ogonek", "caron", "Lslash", "lslash", "Scaron", "scaron", "Zcaron", "zcaron", "brokenbar", "Eth", "eth", "Yacute", "yacute", "Thorn", "thorn", "minus", "multiply", "onesuperior", "twosuperior", "threesuperior", "onehalf", "onequarter", "threequarters", "franc", "Gbreve", "gbreve", "Idotaccent", "Scedilla", "scedilla", "Cacute", "cacute", "Ccaron", "ccaron", "dcroat" }; + + public PostNameRecord[] PostRecords { get; } + + public PostTable( + ushort formatMajor, + ushort formatMinor, + short underlinePosition, + short underlineThickness, + float italicAngle, + uint isFixedPitch, + uint minMemType42, + uint maxMemType42, + uint minMemType1, + uint maxMemType1, + PostNameRecord[] postRecords) + { + this.FormatMajor = formatMajor; + this.FormatMinor = formatMinor; + this.UnderlinePosition = underlinePosition; + this.UnderlineThickness = underlineThickness; + this.ItalicAngle = italicAngle; + this.IsFixedPitch = isFixedPitch; + this.MinMemType42 = minMemType42; + this.MaxMemType42 = maxMemType42; + this.MinMemType1 = minMemType1; + this.MaxMemType1 = maxMemType1; + this.PostRecords = postRecords; + } + + public ushort FormatMajor { get; } + + public ushort FormatMinor { get; } + + public short UnderlinePosition { get; } + + public short UnderlineThickness { get; } + + public float ItalicAngle { get; } + + public uint IsFixedPitch { get; } + + public uint MinMemType42 { get; } + + public uint MaxMemType42 { get; } + + public uint MinMemType1 { get; } + + public uint MaxMemType1 { get; } + + public static PostTable? Load(FontReader fontReader) + { + if (!fontReader.TryGetReaderAtTablePosition(TableName, out BigEndianBinaryReader? binaryReader)) + { + return null; + } + + using (binaryReader) + { + return Load(binaryReader); + } + } + + public static PostTable Load(BigEndianBinaryReader reader) + { + // HEADER + // Type | Name | Description + // ----------------|---------------------|--------------------------------------------------------------- + // Version16Dot16 | version | 0x00010000 for version 1.0, 0x00020000 for version 2.0, 0x00025000 for version 2.5 (deprecated), 0x00030000 for version 3.0 + // Fixed | italicAngle | Italic angle in counter-clockwise degrees from the vertical. Zero for upright text, negative for text that leans to the right (forward). + // FWORD | underlinePosition | This is the suggested distance of the top of the underline from the baseline (negative values indicate below baseline). The PostScript definition of this FontInfo dictionary key (the y coordinate of the center of the stroke) is not used for historical reasons. The value of the PostScript key may be calculated by subtracting half the underlineThickness from the value of this field. + // FWORD | underlineThickness | Suggested values for the underline thickness. In general, the underline thickness should match the thickness of the underscore character (U+005F LOW LINE), and should also match the strikeout thickness, which is specified in the OS/2 table. + // uint32 | isFixedPitch | Set to 0 if the font is proportionally spaced, non-zero if the font is not proportionally spaced (i.e. monospaced). + // uint32 | minMemType42 | Minimum memory usage when an OpenType font is downloaded. + // uint32 | maxMemType42 | Maximum memory usage when an OpenType font is downloaded. + // uint32 | minMemType1 | Minimum memory usage when an OpenType font is downloaded as a Type 1 font. + // uint32 | maxMemType1 | Maximum memory usage when an OpenType font is downloaded as a Type 1 font. + + // FORMAT 2.0 + // Type | Name | Description + // --------|-----------------------------|-------------------------------------------------------------- + // uint16 | numGlyphs | Number of glyphs (this should be the same as numGlyphs in 'maxp' table). + // uint16 | glyphNameIndex[numGlyphs] | Array of indices into the string data. See below for details. + // uint8 | stringData[variable] | Storage for the string data. + ushort formatMajor = reader.ReadUInt16(); + ushort formatMinor = reader.ReadUInt16(); + float italicAngle = reader.ReadFixed(); + short underlinePosition = reader.ReadFWORD(); + short underlineThickness = reader.ReadFWORD(); + uint isFixedPitch = reader.ReadUInt32(); + uint minMemType42 = reader.ReadUInt32(); + uint maxMemType42 = reader.ReadUInt32(); + uint minMemType1 = reader.ReadUInt32(); + uint maxMemType1 = reader.ReadUInt32(); + + var records = new PostNameRecord[0]; + + if (formatMajor == 1) + { + // Supported, no extra subtables needed + } + else if (formatMajor == 2 && formatMinor == 0) + { + ushort numGlyphs = reader.ReadUInt16(); + records = new PostNameRecord[numGlyphs]; + + ushort[] glyphIndices = reader.ReadUInt16Array(numGlyphs); + + for (int i = 0; i < numGlyphs; i++) + { + ushort glyphNameIndex = glyphIndices[i]; + string name; + + // < 258 is a standard fixed apple mapping + if (glyphNameIndex <= 257) + { + name = AppleGlyphNameMap[glyphNameIndex]; + } + else + { + byte strLength = reader.ReadByte(); + name = reader.ReadString(strLength, System.Text.Encoding.ASCII); + } + + records[i] = new PostNameRecord(glyphNameIndex, name); + } + } + else + { + throw new NotSupportedException($"{TableName} table format {formatMajor}.{formatMinor} is not supported."); + } + + return new PostTable( + formatMajor, + formatMinor, + underlinePosition, + underlineThickness, + italicAngle, + isFixedPitch, + minMemType42, + maxMemType42, + minMemType1, + maxMemType1, + records); + } + + public string? GetPostScriptName(int nameIndex) + { + if (this.PostRecords is not null) + { + for (int i = 0; i < this.PostRecords.Length; i++) + { + PostNameRecord p = this.PostRecords[i]; + + if (p.NameIndex == nameIndex) + { + return p.Name; + } + } + } + + return null; + } + } +} diff --git a/tests/SixLabors.Fonts.Tests/BigEndianBinaryWriter.cs b/tests/SixLabors.Fonts.Tests/BigEndianBinaryWriter.cs index 04d7d7afe..e74fa84a3 100644 --- a/tests/SixLabors.Fonts.Tests/BigEndianBinaryWriter.cs +++ b/tests/SixLabors.Fonts.Tests/BigEndianBinaryWriter.cs @@ -380,5 +380,23 @@ public void WriteInt8(sbyte value) public void WriteFWORD(short value) => this.WriteInt16(value); public void WriteUFWORD(ushort value) => this.WriteUInt16(value); + + /// + /// Writes a Pascal string to the stream + /// + /// String value. + /// Encoding type. + public void WriteString(string value, Encoding encoding) + { + // Variable length + byte[] data = encoding.GetBytes(value); + + // Write the length of the data + this.buffer[0] = unchecked((byte)data.Length); + this.WriteInternal(this.buffer, 1); + + // Write the actual string data + this.WriteInternal(data, data.Length); + } } } diff --git a/tests/SixLabors.Fonts.Tests/WriterExtensions.cs b/tests/SixLabors.Fonts.Tests/WriterExtensions.cs index e15d2135b..15cb9e60c 100644 --- a/tests/SixLabors.Fonts.Tests/WriterExtensions.cs +++ b/tests/SixLabors.Fonts.Tests/WriterExtensions.cs @@ -9,6 +9,7 @@ using SixLabors.Fonts.Tables; using SixLabors.Fonts.Tables.General; using SixLabors.Fonts.Tables.General.CMap; +using SixLabors.Fonts.Tables.General.Post; using SixLabors.Fonts.WellKnownIds; namespace SixLabors.Fonts.Tests @@ -320,6 +321,53 @@ public static void WriteCMapSubTable(this BigEndianBinaryWriter writer, Format4S } } + public static void WritePostTable(this BigEndianBinaryWriter writer, PostTable postTable) + { + + // HEADER + // Type | Name | Description + // ----------------|---------------------|--------------------------------------------------------------- + // Version16Dot16 | version | 0x00010000 for version 1.0, 0x00020000 for version 2.0, 0x00025000 for version 2.5 (deprecated), 0x00030000 for version 3.0 + // Fixed | italicAngle | Italic angle in counter-clockwise degrees from the vertical. Zero for upright text, negative for text that leans to the right (forward). + // FWORD | underlinePosition | This is the suggested distance of the top of the underline from the baseline (negative values indicate below baseline). The PostScript definition of this FontInfo dictionary key (the y coordinate of the center of the stroke) is not used for historical reasons. The value of the PostScript key may be calculated by subtracting half the underlineThickness from the value of this field. + // FWORD | underlineThickness | Suggested values for the underline thickness. In general, the underline thickness should match the thickness of the underscore character (U+005F LOW LINE), and should also match the strikeout thickness, which is specified in the OS/2 table. + // uint32 | isFixedPitch | Set to 0 if the font is proportionally spaced, non-zero if the font is not proportionally spaced (i.e. monospaced). + // uint32 | minMemType42 | Minimum memory usage when an OpenType font is downloaded. + // uint32 | maxMemType42 | Maximum memory usage when an OpenType font is downloaded. + // uint32 | minMemType1 | Minimum memory usage when an OpenType font is downloaded as a Type 1 font. + // uint32 | maxMemType1 | Maximum memory usage when an OpenType font is downloaded as a Type 1 font. + writer.WriteUInt16(postTable.FormatMajor); + writer.WriteUInt16(postTable.FormatMinor); + + writer.WriteFWORD(postTable.UnderlinePosition); + writer.WriteFWORD(postTable.UnderlineThickness); + writer.WriteUInt32(postTable.IsFixedPitch); + writer.WriteUInt32(postTable.MinMemType42); + writer.WriteUInt32(postTable.MaxMemType42); + writer.WriteUInt32(postTable.MinMemType1); + writer.WriteUInt32(postTable.MaxMemType1); + + // FORMAT 2.0 + // Type | Name | Description + // --------|-----------------------------|-------------------------------------------------------------- + // uint16 | numGlyphs | Number of glyphs (this should be the same as numGlyphs in 'maxp' table). + // uint16 | glyphNameIndex[numGlyphs] | Array of indices into the string data. See below for details. + // uint8 | stringData[variable] | Storage for the string data. + writer.WriteUInt16((ushort)postTable.PostRecords.Length); + + // Write the array of glyph name indices + foreach (PostNameRecord postRecord in postTable.PostRecords) + { + writer.WriteUInt16(postRecord.NameIndex); + } + + // Write the actual name string data + foreach (PostNameRecord postRecord in postTable.PostRecords) + { + writer.WriteString(postRecord.Name, Encoding.ASCII); + } + } + private static int DataLength(this CMapSubTable subtable) { if (subtable is Format0SubTable table) From 9790ad6eb62919c5287468ff0754a4de2746403e Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 4 Mar 2022 16:15:45 +1100 Subject: [PATCH 12/48] Wire up post table --- src/SixLabors.Fonts/FileFontMetrics.cs | 9 +++ src/SixLabors.Fonts/FontMetrics.cs | 16 ++++++ src/SixLabors.Fonts/StreamFontMetrics.cs | 57 +++++++++---------- .../Post/{PostRecord.cs => PostNameRecord.cs} | 0 .../Tables/General/PostTable.cs | 34 ++++++++--- src/SixLabors.Fonts/Tables/TableLoader.cs | 1 + .../Fakes/FakeFontInstance.cs | 13 ++++- 7 files changed, 91 insertions(+), 39 deletions(-) rename src/SixLabors.Fonts/Tables/General/Post/{PostRecord.cs => PostNameRecord.cs} (100%) diff --git a/src/SixLabors.Fonts/FileFontMetrics.cs b/src/SixLabors.Fonts/FileFontMetrics.cs index e09101906..63ab45894 100644 --- a/src/SixLabors.Fonts/FileFontMetrics.cs +++ b/src/SixLabors.Fonts/FileFontMetrics.cs @@ -100,6 +100,15 @@ internal FileFontMetrics(FontDescription description, string path, long offset) /// public override short StrikeoutPosition => this.metrics.Value.StrikeoutPosition; + /// + public override short UnderlinePosition => this.metrics.Value.UnderlinePosition; + + /// + public override short UnderlineThickness => this.metrics.Value.UnderlineThickness; + + /// + public override float ItalicAngle => this.metrics.Value.ItalicAngle; + /// internal override bool TryGetGlyphId(CodePoint codePoint, out ushort glyphId) => this.metrics.Value.TryGetGlyphId(codePoint, out glyphId); diff --git a/src/SixLabors.Fonts/FontMetrics.cs b/src/SixLabors.Fonts/FontMetrics.cs index 616e80ce5..72d706ee7 100644 --- a/src/SixLabors.Fonts/FontMetrics.cs +++ b/src/SixLabors.Fonts/FontMetrics.cs @@ -117,6 +117,22 @@ internal FontMetrics() /// public abstract short StrikeoutPosition { get; } + /// + /// Gets the suggested distance of the top of the underline from the baseline (negative values indicate below baseline). + /// + public abstract short UnderlinePosition { get; } + + /// + /// Gets the suggested values for the underline thickness. In general, the underline thickness should match the thickness of + /// the underscore character (U+005F LOW LINE), and should also match the strikeout thickness, which is specified in the OS/2 table. + /// + public abstract short UnderlineThickness { get; } + + /// + /// Gets the italic angle in counter-clockwise degrees from the vertical. Zero for upright text, negative for text that leans to the right (forward). + /// + public abstract float ItalicAngle { get; } + /// /// Gets the specified glyph id matching the codepoint. /// diff --git a/src/SixLabors.Fonts/StreamFontMetrics.cs b/src/SixLabors.Fonts/StreamFontMetrics.cs index 114de0328..65099097b 100644 --- a/src/SixLabors.Fonts/StreamFontMetrics.cs +++ b/src/SixLabors.Fonts/StreamFontMetrics.cs @@ -24,6 +24,7 @@ namespace SixLabors.Fonts /// internal class StreamFontMetrics : FontMetrics { + // https://docs.microsoft.com/en-us/typography/opentype/spec/otff#font-tables private readonly MaximumProfileTable maximumProfileTable; private readonly CMapTable cmap; private readonly GlyphTable glyphs; @@ -33,7 +34,7 @@ internal class StreamFontMetrics : FontMetrics private readonly VerticalMetricsTable? verticalMetricsTable; private readonly GlyphMetrics[][] glyphCache; private readonly GlyphMetrics[][]? colorGlyphCache; - private readonly KerningTable kerningTable; + private readonly KerningTable? kerningTable; private readonly GSubTable? gSubTable; private readonly GPosTable? gPosTable; private readonly ColrTable? colrTable; @@ -60,6 +61,7 @@ internal class StreamFontMetrics : FontMetrics /// The vertical metrics table. /// The head table. /// The kerning table. + /// The table needed to use TrueType or OpenType fonts on PostScript printers. /// The glyph substitution table. /// The glyph positioning table. /// The COLR table @@ -79,7 +81,8 @@ internal StreamFontMetrics( VerticalHeadTable? verticalHeadTable, VerticalMetricsTable? verticalMetrics, HeadTable head, - KerningTable kern, + KerningTable? kern, + PostTable postTable, GSubTable? gSubTable, GPosTable? gPosTable, ColrTable? colrTable, @@ -163,6 +166,10 @@ internal StreamFontMetrics( this.StrikeoutSize = os2.StrikeoutSize; this.StrikeoutPosition = os2.StrikeoutPosition; + this.UnderlinePosition = postTable.UnderlinePosition; + this.UnderlinePosition = postTable.UnderlinePosition; + this.ItalicAngle = postTable.ItalicAngle; + this.kerningTable = kern; this.gSubTable = gSubTable; this.gPosTable = gPosTable; @@ -232,6 +239,15 @@ internal StreamFontMetrics( /// public override short StrikeoutPosition { get; } + /// + public override short UnderlinePosition { get; } + + /// + public override short UnderlineThickness { get; } + + /// + public override float ItalicAngle { get; } + /// internal override bool TryGetGlyphId(CodePoint codePoint, out ushort glyphId) => this.TryGetGlyphId(codePoint, null, out glyphId, out bool _); @@ -244,24 +260,14 @@ internal override bool TryGetGlyphId(CodePoint codePoint, CodePoint? nextCodePoi internal override bool TryGetGlyphClass(ushort glyphId, [NotNullWhen(true)] out GlyphClassDef? glyphClass) { glyphClass = null; - if (this.glyphDefinitionTable is not null && this.glyphDefinitionTable.TryGetGlyphClass(glyphId, out glyphClass)) - { - return true; - } - - return false; + return this.glyphDefinitionTable is not null && this.glyphDefinitionTable.TryGetGlyphClass(glyphId, out glyphClass); } /// internal override bool TryGetMarkAttachmentClass(ushort glyphId, [NotNullWhen(true)] out GlyphClassDef? markAttachmentClass) { markAttachmentClass = null; - if (this.glyphDefinitionTable is not null && this.glyphDefinitionTable.TryGetMarkAttachmentClass(glyphId, out markAttachmentClass)) - { - return true; - } - - return false; + return this.glyphDefinitionTable is not null && this.glyphDefinitionTable.TryGetMarkAttachmentClass(glyphId, out markAttachmentClass); } /// @@ -305,22 +311,14 @@ internal override IEnumerable GetGlyphMetrics(CodePoint codePoint, /// internal override void ApplySubstitution(GlyphSubstitutionCollection collection) - { - if (this.gSubTable != null) - { - this.gSubTable.ApplySubstitution(this, collection); - } - } + => this.gSubTable?.ApplySubstitution(this, collection); /// internal override void UpdatePositions(GlyphPositioningCollection collection) { bool kerned = false; KerningMode kerningMode = collection.TextOptions.KerningMode; - if (this.gPosTable != null) - { - this.gPosTable.TryUpdatePositions(this, collection, out kerned); - } + this.gPosTable?.TryUpdatePositions(this, collection, out kerned); if (!kerned && kerningMode != KerningMode.None && this.kerningTable != null) { @@ -398,9 +396,13 @@ internal static StreamFontMetrics LoadFont(FontReader reader) reader.GetTable(); // loca GlyphTable glyphs = reader.GetTable(); // glyf - KerningTable kern = reader.GetTable(); // kern - Kerning + KerningTable? kern = reader.TryGetTable(); // kern - Kerning NameTable nameTable = reader.GetTable(); // name + PostTable postTable = reader.GetTable(); // post - PostScript information + // gasp - Grid-fitting/Scan-conversion (optional table) + // PCLT - PCL 5 data + // DSIG - Digital signature ColrTable? colrTable = reader.TryGetTable(); // colr CpalTable? cpalTable; if (colrTable != null) @@ -417,10 +419,6 @@ internal static StreamFontMetrics LoadFont(FontReader reader) GPosTable? gPos = reader.TryGetTable(); GlyphDefinitionTable? glyphDefinitionTable = reader.TryGetTable(); - // post - PostScript information - // gasp - Grid-fitting/Scan-conversion (optional table) - // PCLT - PCL 5 data - // DSIG - Digital signature return new StreamFontMetrics( nameTable, maxp, @@ -433,6 +431,7 @@ internal static StreamFontMetrics LoadFont(FontReader reader) verticalMetrics, head, kern, + postTable, gSub, gPos, colrTable, diff --git a/src/SixLabors.Fonts/Tables/General/Post/PostRecord.cs b/src/SixLabors.Fonts/Tables/General/Post/PostNameRecord.cs similarity index 100% rename from src/SixLabors.Fonts/Tables/General/Post/PostRecord.cs rename to src/SixLabors.Fonts/Tables/General/Post/PostNameRecord.cs diff --git a/src/SixLabors.Fonts/Tables/General/PostTable.cs b/src/SixLabors.Fonts/Tables/General/PostTable.cs index c6bb6ced3..c63fdee52 100644 --- a/src/SixLabors.Fonts/Tables/General/PostTable.cs +++ b/src/SixLabors.Fonts/Tables/General/PostTable.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System; +using System.Text; using SixLabors.Fonts.Tables.General.Post; namespace SixLabors.Fonts.Tables.General @@ -9,11 +10,27 @@ namespace SixLabors.Fonts.Tables.General [TableName(TableName)] internal class PostTable : Table { - internal const string TableName = "POST"; - - private static readonly string[] AppleGlyphNameMap = new[] { ".notdef", ".null", "nonmarkingreturn", "space", "exclam", "quotedbl", "numbersign", "dollar", "percent", "ampersand", "quotesingle", "parenleft", "parenright", "asterisk", "plus", "comma", "hyphen", "period", "slash", "zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "colon", "semicolon", "less", "equal", "greater", "question", "at", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "bracketleft", "backslash", "bracketright", "asciicircum", "underscore", "grave", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "braceleft", "bar", "braceright", "asciitilde", "Adieresis", "Aring", "Ccedilla", "Eacute", "Ntilde", "Odieresis", "Udieresis", "aacute", "agrave", "acircumflex", "adieresis", "atilde", "aring", "ccedilla", "eacute", "egrave", "ecircumflex", "edieresis", "iacute", "igrave", "icircumflex", "idieresis", "ntilde", "oacute", "ograve", "ocircumflex", "odieresis", "otilde", "uacute", "ugrave", "ucircumflex", "udieresis", "dagger", "degree", "cent", "sterling", "section", "bullet", "paragraph", "germandbls", "registered", "copyright", "trademark", "acute", "dieresis", "notequal", "AE", "Oslash", "infinity", "plusminus", "lessequal", "greaterequal", "yen", "mu", "partialdiff", "summation", "product", "pi", "integral", "ordfeminine", "ordmasculine", "Omega", "ae", "oslash", "questiondown", "exclamdown", "logicalnot", "radical", "florin", "approxequal", "Delta", "guillemotleft", "guillemotright", "ellipsis", "nonbreakingspace", "Agrave", "Atilde", "Otilde", "OE", "oe", "endash", "emdash", "quotedblleft", "quotedblright", "quoteleft", "quoteright", "divide", "lozenge", "ydieresis", "Ydieresis", "fraction", "currency", "guilsinglleft", "guilsinglright", "fi", "fl", "daggerdbl", "periodcentered", "quotesinglbase", "quotedblbase", "perthousand", "Acircumflex", "Ecircumflex", "Aacute", "Edieresis", "Egrave", "Iacute", "Icircumflex", "Idieresis", "Igrave", "Oacute", "Ocircumflex", "apple", "Ograve", "Uacute", "Ucircumflex", "Ugrave", "dotlessi", "circumflex", "tilde", "macron", "breve", "dotaccent", "ring", "cedilla", "hungarumlaut", "ogonek", "caron", "Lslash", "lslash", "Scaron", "scaron", "Zcaron", "zcaron", "brokenbar", "Eth", "eth", "Yacute", "yacute", "Thorn", "thorn", "minus", "multiply", "onesuperior", "twosuperior", "threesuperior", "onehalf", "onequarter", "threequarters", "franc", "Gbreve", "gbreve", "Idotaccent", "Scedilla", "scedilla", "Cacute", "cacute", "Ccaron", "ccaron", "dcroat" }; - - public PostNameRecord[] PostRecords { get; } + internal const string TableName = "post"; + private static readonly string[] AppleGlyphNameMap + = new[] + { + ".notdef", ".null", "nonmarkingreturn", "space", "exclam", "quotedbl", "numbersign", "dollar", "percent", "ampersand", "quotesingle", "parenleft", "parenright", + "asterisk", "plus", "comma", "hyphen", "period", "slash", "zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "colon", "semicolon", + "less", "equal", "greater", "question", "at", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", + "X", "Y", "Z", "bracketleft", "backslash", "bracketright", "asciicircum", "underscore", "grave", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", + "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "braceleft", "bar", "braceright", "asciitilde", "Adieresis", "Aring", "Ccedilla", "Eacute", + "Ntilde", "Odieresis", "Udieresis", "aacute", "agrave", "acircumflex", "adieresis", "atilde", "aring", "ccedilla", "eacute", "egrave", "ecircumflex", "edieresis", + "iacute", "igrave", "icircumflex", "idieresis", "ntilde", "oacute", "ograve", "ocircumflex", "odieresis", "otilde", "uacute", "ugrave", "ucircumflex", "udieresis", + "dagger", "degree", "cent", "sterling", "section", "bullet", "paragraph", "germandbls", "registered", "copyright", "trademark", "acute", "dieresis", "notequal", + "AE", "Oslash", "infinity", "plusminus", "lessequal", "greaterequal", "yen", "mu", "partialdiff", "summation", "product", "pi", "integral", "ordfeminine", + "ordmasculine", "Omega", "ae", "oslash", "questiondown", "exclamdown", "logicalnot", "radical", "florin", "approxequal", "Delta", "guillemotleft", "guillemotright", + "ellipsis", "nonbreakingspace", "Agrave", "Atilde", "Otilde", "OE", "oe", "endash", "emdash", "quotedblleft", "quotedblright", "quoteleft", "quoteright", "divide", + "lozenge", "ydieresis", "Ydieresis", "fraction", "currency", "guilsinglleft", "guilsinglright", "fi", "fl", "daggerdbl", "periodcentered", "quotesinglbase", "quotedblbase", + "perthousand", "Acircumflex", "Ecircumflex", "Aacute", "Edieresis", "Egrave", "Iacute", "Icircumflex", "Idieresis", "Igrave", "Oacute", "Ocircumflex", "apple", "Ograve", + "Uacute", "Ucircumflex", "Ugrave", "dotlessi", "circumflex", "tilde", "macron", "breve", "dotaccent", "ring", "cedilla", "hungarumlaut", "ogonek", "caron", "Lslash", + "lslash", "Scaron", "scaron", "Zcaron", "zcaron", "brokenbar", "Eth", "eth", "Yacute", "yacute", "Thorn", "thorn", "minus", "multiply", "onesuperior", "twosuperior", + "threesuperior", "onehalf", "onequarter", "threequarters", "franc", "Gbreve", "gbreve", "Idotaccent", "Scedilla", "scedilla", "Cacute", "cacute", "Ccaron", "ccaron", "dcroat" + }; public PostTable( ushort formatMajor, @@ -41,6 +58,8 @@ public PostTable( this.PostRecords = postRecords; } + public PostNameRecord[] PostRecords { get; } + public ushort FormatMajor { get; } public ushort FormatMinor { get; } @@ -132,17 +151,18 @@ public static PostTable Load(BigEndianBinaryReader reader) else { byte strLength = reader.ReadByte(); - name = reader.ReadString(strLength, System.Text.Encoding.ASCII); + name = reader.ReadString(strLength, Encoding.ASCII); } records[i] = new PostNameRecord(glyphNameIndex, name); } } - else + else if (formatMajor > 3) { throw new NotSupportedException($"{TableName} table format {formatMajor}.{formatMinor} is not supported."); } + // TODO: Validate maximum numbers against maxp table. return new PostTable( formatMajor, formatMinor, diff --git a/src/SixLabors.Fonts/Tables/TableLoader.cs b/src/SixLabors.Fonts/Tables/TableLoader.cs index a96d251cb..a64607e91 100644 --- a/src/SixLabors.Fonts/Tables/TableLoader.cs +++ b/src/SixLabors.Fonts/Tables/TableLoader.cs @@ -40,6 +40,7 @@ public TableLoader() this.Register(FpgmTable.Load); this.Register(PrepTable.Load); this.Register(GlyphDefinitionTable.Load); + this.Register(PostTable.Load); } public static TableLoader Default { get; } = new(); diff --git a/tests/SixLabors.Fonts.Tests/Fakes/FakeFontInstance.cs b/tests/SixLabors.Fonts.Tests/Fakes/FakeFontInstance.cs index 08ff7b237..27ed3c79d 100644 --- a/tests/SixLabors.Fonts.Tests/Fakes/FakeFontInstance.cs +++ b/tests/SixLabors.Fonts.Tests/Fakes/FakeFontInstance.cs @@ -6,6 +6,7 @@ using System.Linq; using SixLabors.Fonts.Tables.General; using SixLabors.Fonts.Tables.General.Kern; +using SixLabors.Fonts.Tables.General.Post; using SixLabors.Fonts.Unicode; namespace SixLabors.Fonts.Tests.Fakes @@ -30,6 +31,7 @@ internal FakeFontInstance(List glyphs) GenerateVerticalMetricsTable(glyphs), GenerateHeadTable(), new KerningTable(Array.Empty()), + GeneratePostTable(), null, null, null, @@ -52,7 +54,8 @@ internal FakeFontInstance( VerticalHeadTable verticalHeadTable, VerticalMetricsTable verticalMetrics, HeadTable head, - KerningTable kern) + KerningTable kern, + PostTable post) : base( nameTable, maxpTable, @@ -65,6 +68,7 @@ internal FakeFontInstance( verticalMetrics, head, kern, + post, null, null, null, @@ -94,7 +98,8 @@ public static FakeFontInstance CreateFontWithVaryingVerticalFontMetrics(string t GenerateVerticalHeadTable(), GenerateVerticalMetricsTable(glyphs), GenerateHeadTable(), - new KerningTable(Array.Empty())); + new KerningTable(Array.Empty()), + GeneratePostTable()); } private static List GetGlyphs(string text) @@ -128,7 +133,7 @@ private static CMapTable GenerateCMapTable(List glyphs) => new(new[] { new FakeCmapSubtable(glyphs) }); private static MaximumProfileTable GenerateMaxpTable(List glyphs) - => new MaximumProfileTable((ushort)glyphs.Count); + => new((ushort)glyphs.Count); private static HorizontalHeadTable GenerateHorizontalHeadTable() => new(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1); @@ -158,5 +163,7 @@ private static HeadTable GenerateHeadTable() new Bounds(10, 10, 20, 20), 1, HeadTable.IndexLocationFormats.Offset16); + + private static PostTable GeneratePostTable() => new(2, 0, 0, 200, 35, 0, 0, 0, 0, 0, Array.Empty()); } } From afa057d1dc6a02d00492f439e3b21293ebd09836 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 4 Mar 2022 16:18:34 +1100 Subject: [PATCH 13/48] Add TODO: notes --- src/SixLabors.Fonts/GlyphMetrics.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/SixLabors.Fonts/GlyphMetrics.cs b/src/SixLabors.Fonts/GlyphMetrics.cs index 65e7e5a5a..03ea72493 100644 --- a/src/SixLabors.Fonts/GlyphMetrics.cs +++ b/src/SixLabors.Fonts/GlyphMetrics.cs @@ -185,6 +185,7 @@ internal void ApplyAdvance(short x, short y) internal FontRectangle GetBoundingBox(Vector2 origin, float scaledPointSize) { + // TODO: This should be scaled for subscript/superscript. Bounds bounds = this.GetBounds(); Vector2 size = bounds.Size() * scaledPointSize / this.ScaleFactor; Vector2 loc = new Vector2(bounds.Min.X, bounds.Max.Y) * scaledPointSize / this.ScaleFactor * MirrorScale; @@ -303,6 +304,8 @@ public void RenderTo(IGlyphRenderer surface, float pointSize, Vector2 location, surface.EndFigure(); } + + // TODO: Draw rectangles for strikethrough, underline. } surface.EndGlyph(); From 45b5c9c437ce909188f51785a50cb991558dc969 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 4 Mar 2022 21:47:24 +1100 Subject: [PATCH 14/48] Superscript/Subscript scaling works. TODO: Location offsets --- src/SixLabors.Fonts/GlyphMetrics.cs | 24 +++++++++++++++---- .../Tables/General/Glyphs/GlyphVector.cs | 9 +++++++ src/SixLabors.Fonts/TextLayout.cs | 14 +++++------ .../SixLabors.Fonts.Tests/FontMetricsTests.cs | 9 +++---- .../SixLabors.Fonts.Tests/WriterExtensions.cs | 1 - 5 files changed, 40 insertions(+), 17 deletions(-) diff --git a/src/SixLabors.Fonts/GlyphMetrics.cs b/src/SixLabors.Fonts/GlyphMetrics.cs index 03ea72493..662c2488e 100644 --- a/src/SixLabors.Fonts/GlyphMetrics.cs +++ b/src/SixLabors.Fonts/GlyphMetrics.cs @@ -48,7 +48,20 @@ internal GlyphMetrics( this.TextAttributes = textAttributes; this.LeftSideBearing = leftSideBearing; this.TopSideBearing = topSideBearing; - this.ScaleFactor = this.UnitsPerEm * 72F; + + Vector2 scaleFactor = new(unitsPerEM * 72F); + if (this.TextAttributes.HasFlag(TextAttribute.Subscript)) + { + float units = unitsPerEM; + scaleFactor /= new Vector2(this.FontMetrics.SubscriptXSize / units, this.FontMetrics.SubscriptYSize / units); + } + else if (this.TextAttributes.HasFlag(TextAttribute.Superscript)) + { + float units = unitsPerEM; + scaleFactor /= new Vector2(this.FontMetrics.SuperscriptXSize / units, this.FontMetrics.SuperscriptYSize / units); + } + + this.ScaleFactor = scaleFactor; this.GlyphColor = glyphColor; } @@ -132,7 +145,7 @@ internal GlyphMetrics(GlyphMetrics other, CodePoint codePoint) public ushort UnitsPerEm { get; } /// - public float ScaleFactor { get; } + public Vector2 ScaleFactor { get; } /// /// Gets the glyph Id. @@ -186,9 +199,10 @@ internal void ApplyAdvance(short x, short y) internal FontRectangle GetBoundingBox(Vector2 origin, float scaledPointSize) { // TODO: This should be scaled for subscript/superscript. + Vector2 scale = new Vector2(scaledPointSize) / this.ScaleFactor; Bounds bounds = this.GetBounds(); - Vector2 size = bounds.Size() * scaledPointSize / this.ScaleFactor; - Vector2 loc = new Vector2(bounds.Min.X, bounds.Max.Y) * scaledPointSize / this.ScaleFactor * MirrorScale; + Vector2 size = bounds.Size() * scale; + Vector2 loc = new Vector2(bounds.Min.X, bounds.Max.Y) * scale * MirrorScale; loc = origin + loc; @@ -223,7 +237,7 @@ public void RenderTo(IGlyphRenderer surface, float pointSize, Vector2 location, if (!this.scaledVector.TryGetValue(scaledPoint, out GlyphVector scaledVector)) { - scaledVector = GlyphVector.Scale(this.vector, scaledPoint / this.ScaleFactor); + scaledVector = GlyphVector.Scale(this.vector, new Vector2(scaledPoint) / this.ScaleFactor); this.scaledVector[scaledPoint] = scaledVector; } diff --git a/src/SixLabors.Fonts/Tables/General/Glyphs/GlyphVector.cs b/src/SixLabors.Fonts/Tables/General/Glyphs/GlyphVector.cs index 8859ce7d7..6a333c248 100644 --- a/src/SixLabors.Fonts/Tables/General/Glyphs/GlyphVector.cs +++ b/src/SixLabors.Fonts/Tables/General/Glyphs/GlyphVector.cs @@ -72,6 +72,15 @@ public static GlyphVector Transform(GlyphVector src, Matrix3x2 matrix) public static GlyphVector Scale(GlyphVector src, float scale) => Transform(src, Matrix3x2.CreateScale(scale)); + /// + /// Scales a glyph vector uniformly by a specified scale. + /// + /// The glyph vector to translate. + /// The vector scale to use. + /// The new . + public static GlyphVector Scale(GlyphVector src, Vector2 scales) + => Transform(src, Matrix3x2.CreateScale(scales)); + /// /// Translates a glyph vector by a specified x and y coordinates. /// diff --git a/src/SixLabors.Fonts/TextLayout.cs b/src/SixLabors.Fonts/TextLayout.cs index ed6713eb4..d7031d7c5 100644 --- a/src/SixLabors.Fonts/TextLayout.cs +++ b/src/SixLabors.Fonts/TextLayout.cs @@ -311,7 +311,7 @@ private static IEnumerable LayoutLineHorizontal( new Glyph(metric, info.PointSize), location, info.ScaledAdvance, - metric.AdvanceHeight * (info.PointSize / metric.ScaleFactor), + metric.AdvanceHeight * (info.PointSize / metric.ScaleFactor.Y), textBox.ScaledMaxLineHeight * options.LineSpacing, i == 0)); } @@ -428,7 +428,7 @@ private static IEnumerable LayoutLineVertical( { glyphs.Add(new GlyphLayout( new Glyph(metric, info.PointSize), - location + new Vector2((xWidth - (metric.AdvanceWidth * (info.PointSize / metric.ScaleFactor))) * .5F, 0), + location + new Vector2((xWidth - (metric.AdvanceWidth * (info.PointSize / metric.ScaleFactor.X))) * .5F, 0), xLineAdvance, info.ScaledAdvance, textBox.ScaledMaxLineHeight, @@ -671,7 +671,7 @@ private static TextBox BreakLines( } } - glyphAdvance *= pointSize / glyph.ScaleFactor; + glyphAdvance *= pointSize / (isHorizontal ? glyph.ScaleFactor.X : glyph.ScaleFactor.Y); // Should we start a new line? bool requiredBreak = false; @@ -762,10 +762,10 @@ private static TextBox BreakLines( } GlyphMetrics metric = metrics[0]; - float ascender = metric.FontMetrics.Ascender * pointSize / metric.ScaleFactor; - float descender = Math.Abs(metric.FontMetrics.Descender * pointSize / metric.ScaleFactor); - float lineHeight = metric.FontMetrics.LineHeight * pointSize / metric.ScaleFactor; - float leftSideBearing = metric.LeftSideBearing * pointSize / metric.ScaleFactor; + float ascender = metric.FontMetrics.Ascender * pointSize / metric.ScaleFactor.Y; + float descender = Math.Abs(metric.FontMetrics.Descender * pointSize / metric.ScaleFactor.Y); + float lineHeight = metric.FontMetrics.LineHeight * pointSize / metric.ScaleFactor.Y; + float leftSideBearing = metric.LeftSideBearing * pointSize / metric.ScaleFactor.X; if (ascender > scaledMaxAscender) { diff --git a/tests/SixLabors.Fonts.Tests/FontMetricsTests.cs b/tests/SixLabors.Fonts.Tests/FontMetricsTests.cs index eae58bd02..e0ac4e93b 100644 --- a/tests/SixLabors.Fonts.Tests/FontMetricsTests.cs +++ b/tests/SixLabors.Fonts.Tests/FontMetricsTests.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System.Linq; +using System.Numerics; using SixLabors.Fonts.Unicode; using Xunit; @@ -66,7 +67,7 @@ public void GlyphMetricsMatchesReference() Assert.Equal(codePoint, glyphMetrics.CodePoint); Assert.Equal(font.FontMetrics.UnitsPerEm, glyphMetrics.UnitsPerEm); - Assert.Equal(glyphMetrics.UnitsPerEm * 72F, glyphMetrics.ScaleFactor); + Assert.Equal(new Vector2(glyphMetrics.UnitsPerEm * 72F), glyphMetrics.ScaleFactor); Assert.Equal(1295, glyphMetrics.AdvanceWidth); Assert.Equal(2789, glyphMetrics.AdvanceHeight); Assert.Equal(1293, glyphMetrics.Width); @@ -93,7 +94,7 @@ public void GlyphMetricsMatchesReference_WithWoff1format() Assert.Equal(codePoint, glyphMetrics.CodePoint); Assert.Equal(font.FontMetrics.UnitsPerEm, glyphMetrics.UnitsPerEm); - Assert.Equal(glyphMetrics.UnitsPerEm * 72F, glyphMetrics.ScaleFactor); + Assert.Equal(new Vector2(glyphMetrics.UnitsPerEm * 72F), glyphMetrics.ScaleFactor); Assert.Equal(1295, glyphMetrics.AdvanceWidth); Assert.Equal(2789, glyphMetrics.AdvanceHeight); Assert.Equal(1293, glyphMetrics.Width); @@ -121,7 +122,7 @@ public void GlyphMetricsMatchesReference_WithWoff2format() Assert.Equal(codePoint, glyphMetrics.CodePoint); Assert.Equal(font.FontMetrics.UnitsPerEm, glyphMetrics.UnitsPerEm); - Assert.Equal(glyphMetrics.UnitsPerEm * 72F, glyphMetrics.ScaleFactor); + Assert.Equal(new Vector2(glyphMetrics.UnitsPerEm * 72F), glyphMetrics.ScaleFactor); Assert.Equal(1295, glyphMetrics.AdvanceWidth); Assert.Equal(2789, glyphMetrics.AdvanceHeight); Assert.Equal(1293, glyphMetrics.Width); @@ -150,7 +151,7 @@ public void GlyphMetricsVerticalMatchesReference() // Position 0. Assert.Equal(codePoint, glyphMetrics.CodePoint); Assert.Equal(font.FontMetrics.UnitsPerEm, glyphMetrics.UnitsPerEm); - Assert.Equal(glyphMetrics.UnitsPerEm * 72F, glyphMetrics.ScaleFactor); + Assert.Equal(new Vector2(glyphMetrics.UnitsPerEm * 72F), glyphMetrics.ScaleFactor); Assert.Equal(364, glyphMetrics.AdvanceWidth); Assert.Equal(1000, glyphMetrics.AdvanceHeight); Assert.Equal(265, glyphMetrics.Width); diff --git a/tests/SixLabors.Fonts.Tests/WriterExtensions.cs b/tests/SixLabors.Fonts.Tests/WriterExtensions.cs index 15cb9e60c..224e7df8c 100644 --- a/tests/SixLabors.Fonts.Tests/WriterExtensions.cs +++ b/tests/SixLabors.Fonts.Tests/WriterExtensions.cs @@ -323,7 +323,6 @@ public static void WriteCMapSubTable(this BigEndianBinaryWriter writer, Format4S public static void WritePostTable(this BigEndianBinaryWriter writer, PostTable postTable) { - // HEADER // Type | Name | Description // ----------------|---------------------|--------------------------------------------------------------- From 7107da38264d0a839d4b0c721a8a4fb223a3c911 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sat, 5 Mar 2022 00:14:13 +1100 Subject: [PATCH 15/48] Scale and offset subscript + superscript --- src/SixLabors.Fonts/GlyphMetrics.cs | 15 ++++++--- .../GlyphRendererParameters.cs | 1 - src/SixLabors.Fonts/GlyphShapingData.cs | 2 +- src/SixLabors.Fonts/StreamFontMetrics.cs | 25 ++++++++------- src/SixLabors.Fonts/TextLayout.cs | 31 ++++++++++++++----- 5 files changed, 50 insertions(+), 24 deletions(-) diff --git a/src/SixLabors.Fonts/GlyphMetrics.cs b/src/SixLabors.Fonts/GlyphMetrics.cs index 662c2488e..181d26d5f 100644 --- a/src/SixLabors.Fonts/GlyphMetrics.cs +++ b/src/SixLabors.Fonts/GlyphMetrics.cs @@ -17,6 +17,7 @@ public class GlyphMetrics private static readonly Vector2 MirrorScale = new(1, -1); private GlyphVector vector; private readonly Dictionary scaledVector = new(); + private Vector2 offset = Vector2.Zero; internal GlyphMetrics( StreamFontMetrics font, @@ -54,11 +55,13 @@ internal GlyphMetrics( { float units = unitsPerEM; scaleFactor /= new Vector2(this.FontMetrics.SubscriptXSize / units, this.FontMetrics.SubscriptYSize / units); + this.offset = new(this.FontMetrics.SubscriptXOffset, this.FontMetrics.SubscriptYOffset); } else if (this.TextAttributes.HasFlag(TextAttribute.Superscript)) { float units = unitsPerEM; scaleFactor /= new Vector2(this.FontMetrics.SuperscriptXSize / units, this.FontMetrics.SuperscriptYSize / units); + this.offset = new(this.FontMetrics.SuperscriptXOffset, -this.FontMetrics.SuperscriptYOffset); } this.ScaleFactor = scaleFactor; @@ -84,6 +87,7 @@ internal GlyphMetrics(GlyphMetrics other, CodePoint codePoint) this.TopSideBearing = other.TopSideBearing; this.ScaleFactor = other.ScaleFactor; this.GlyphColor = other.GlyphColor; + this.offset = other.offset; } /// @@ -198,12 +202,10 @@ internal void ApplyAdvance(short x, short y) internal FontRectangle GetBoundingBox(Vector2 origin, float scaledPointSize) { - // TODO: This should be scaled for subscript/superscript. Vector2 scale = new Vector2(scaledPointSize) / this.ScaleFactor; Bounds bounds = this.GetBounds(); Vector2 size = bounds.Size() * scale; - Vector2 loc = new Vector2(bounds.Min.X, bounds.Max.Y) * scale * MirrorScale; - + Vector2 loc = (new Vector2(bounds.Min.X, bounds.Max.Y) + this.offset) * scale * MirrorScale; loc = origin + loc; return new FontRectangle(loc.X, loc.Y, size.X, size.Y); @@ -237,7 +239,12 @@ public void RenderTo(IGlyphRenderer surface, float pointSize, Vector2 location, if (!this.scaledVector.TryGetValue(scaledPoint, out GlyphVector scaledVector)) { - scaledVector = GlyphVector.Scale(this.vector, new Vector2(scaledPoint) / this.ScaleFactor); + Vector2 scale = new Vector2(scaledPoint) / this.ScaleFactor; + scaledVector = GlyphVector.Scale(this.vector, scale); + + Vector2 offset = this.offset * scale * MirrorScale; + scaledVector = GlyphVector.Translate(scaledVector, offset.X, offset.Y); + this.scaledVector[scaledPoint] = scaledVector; } diff --git a/src/SixLabors.Fonts/GlyphRendererParameters.cs b/src/SixLabors.Fonts/GlyphRendererParameters.cs index 50d9c390a..62e31ce30 100644 --- a/src/SixLabors.Fonts/GlyphRendererParameters.cs +++ b/src/SixLabors.Fonts/GlyphRendererParameters.cs @@ -3,7 +3,6 @@ using System; using System.Diagnostics; -using System.Numerics; namespace SixLabors.Fonts { diff --git a/src/SixLabors.Fonts/GlyphShapingData.cs b/src/SixLabors.Fonts/GlyphShapingData.cs index 0616ab880..067e820e1 100644 --- a/src/SixLabors.Fonts/GlyphShapingData.cs +++ b/src/SixLabors.Fonts/GlyphShapingData.cs @@ -104,6 +104,6 @@ public GlyphShapingData(GlyphShapingData data, bool clearFeatures = false) private string DebuggerDisplay => FormattableString - .Invariant($"{this.CodePoint.ToDebuggerDisplay()} : {CodePoint.GetScriptClass(this.CodePoint)} : {this.Direction} : {this.LigatureId} : {this.LigatureComponent} : [{string.Join(",", this.GlyphIds)}]"); + .Invariant($"{this.CodePoint.ToDebuggerDisplay()} : {CodePoint.GetScriptClass(this.CodePoint)} : {this.Direction} : {this.TextAttributes} : {this.LigatureId} : {this.LigatureComponent} : [{string.Join(",", this.GlyphIds)}]"); } } diff --git a/src/SixLabors.Fonts/StreamFontMetrics.cs b/src/SixLabors.Fonts/StreamFontMetrics.cs index 65099097b..e1b4e263d 100644 --- a/src/SixLabors.Fonts/StreamFontMetrics.cs +++ b/src/SixLabors.Fonts/StreamFontMetrics.cs @@ -289,12 +289,14 @@ internal override IEnumerable GetGlyphMetrics(CodePoint codePoint, } if (support == ColorFontSupport.MicrosoftColrFormat - && this.TryGetColoredVectors(codePoint, glyphId, textAttributes, out GlyphMetrics[]? metrics)) + && this.TryGetColoredMetrics(codePoint, glyphId, textAttributes, out GlyphMetrics[]? metrics)) { return metrics; } - if (this.glyphCache[glyphId] is null) + // We overwrite the cache entry for this type should the attributes change. + GlyphMetrics[]? cached = this.glyphCache[glyphId]; + if (cached == null || cached[0]!.TextAttributes != textAttributes) { this.glyphCache[glyphId] = new[] { @@ -510,34 +512,35 @@ internal GlyphVector ApplyHinting(GlyphVector glyphVector, float pixelSize, usho return glyphVector; } - private bool TryGetColoredVectors(CodePoint codePoint, ushort glyphId, TextAttribute textAttributes, [NotNullWhen(true)] out GlyphMetrics[]? vectors) + private bool TryGetColoredMetrics(CodePoint codePoint, ushort glyphId, TextAttribute textAttributes, [NotNullWhen(true)] out GlyphMetrics[]? metrics) { if (this.colrTable == null || this.colorGlyphCache == null) { - vectors = null; + metrics = null; return false; } - vectors = this.colorGlyphCache[glyphId]; - if (vectors is null) + // We overwrite the cache entry for this type should the attributes change. + metrics = this.colorGlyphCache[glyphId]; + if (metrics == null || metrics[0].TextAttributes != textAttributes) { Span indexes = this.colrTable.GetLayers(glyphId); if (indexes.Length > 0) { - vectors = new GlyphMetrics[indexes.Length]; + metrics = new GlyphMetrics[indexes.Length]; for (int i = 0; i < indexes.Length; i++) { LayerRecord? layer = indexes[i]; - vectors[i] = this.CreateGlyphMetrics(codePoint, layer.GlyphId, GlyphType.ColrLayer, textAttributes, layer.PaletteIndex); + metrics[i] = this.CreateGlyphMetrics(codePoint, layer.GlyphId, GlyphType.ColrLayer, textAttributes, layer.PaletteIndex); } } - vectors ??= Array.Empty(); - this.colorGlyphCache[glyphId] = vectors; + metrics ??= Array.Empty(); + this.colorGlyphCache[glyphId] = metrics; } - return vectors.Length > 0; + return metrics.Length > 0; } private GlyphMetrics CreateGlyphMetrics( diff --git a/src/SixLabors.Fonts/TextLayout.cs b/src/SixLabors.Fonts/TextLayout.cs index d7031d7c5..49e14e0b3 100644 --- a/src/SixLabors.Fonts/TextLayout.cs +++ b/src/SixLabors.Fonts/TextLayout.cs @@ -79,12 +79,23 @@ public static IReadOnlyList BuildTextRuns(ReadOnlySpan text, Text // Add a final run if required. if (start < end) { - textRuns.Add(new() + // Offset error by user, last index in input string + // instead of exclusive index. + if (start == end - 1) { - Start = start, - End = end, - Font = options.Font - }); + int prevIndex = textRuns.Count - 1; + TextRun previous = textRuns[prevIndex]; + previous.End++; + } + else + { + textRuns.Add(new() + { + Start = start, + End = end, + Font = options.Font + }); + } } return textRuns; @@ -126,12 +137,15 @@ private static TextBox ProcessText(ReadOnlySpan text, TextOptions options) // First do multiple font runs using the individual text runs. bool complete = true; + int runIndex = 0; foreach (TextRun textRun in textRuns) { + ref int index = ref runIndex; if (!DoFontRun( textRun.Slice(text), textRun.Start, textRuns, + ref index, false, textRun.Font!, bidiRuns, @@ -149,10 +163,13 @@ private static TextBox ProcessText(ReadOnlySpan text, TextOptions options) // We do a complete run here across the whole collection. foreach (Font font in fallbackFonts) { + runIndex = 0; + ref int index = ref runIndex; if (DoFontRun( text, 0, textRuns, + ref index, true, font, bidiRuns, @@ -451,6 +468,7 @@ private static bool DoFontRun( ReadOnlySpan text, int start, IReadOnlyList textRuns, + ref int textRun, bool isFallbackRun, Font font, BidiRun[] bidiRuns, @@ -466,7 +484,6 @@ private static bool DoFontRun( int graphemeIndex; int codePointIndex = start; int bidiRun = 0; - int textRun = 0; var graphemeEnumerator = new SpanGraphemeEnumerator(text); for (graphemeIndex = 0; graphemeEnumerator.MoveNext(); graphemeIndex++) { @@ -517,7 +534,7 @@ private static bool DoFontRun( } // Apply the simple and complex substitutions. - // TODO: Investigate HarfBuzz normlizer. + // TODO: Investigate HarfBuzz normalizer. SubstituteBidiMirrors(font.FontMetrics, substitutions); font.FontMetrics.ApplySubstitution(substitutions); From 91916c786edd1e636509f216896b6f4652fc2a15 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sat, 5 Mar 2022 00:56:02 +1100 Subject: [PATCH 16/48] Add TextRun available to GlyphRendererParameters --- src/SixLabors.Fonts/GlyphMetrics.cs | 7 +++++-- src/SixLabors.Fonts/GlyphPositioningCollection.cs | 12 ++++++++---- src/SixLabors.Fonts/GlyphRendererParameters.cs | 10 ++++++++-- src/SixLabors.Fonts/GlyphShapingData.cs | 12 +++++------- src/SixLabors.Fonts/GlyphSubstitutionCollection.cs | 7 +++---- .../Tables/AdvancedTypographic/GPos/AnchorTable.cs | 2 +- src/SixLabors.Fonts/TextLayout.cs | 2 +- 7 files changed, 31 insertions(+), 21 deletions(-) diff --git a/src/SixLabors.Fonts/GlyphMetrics.cs b/src/SixLabors.Fonts/GlyphMetrics.cs index 181d26d5f..974a39c85 100644 --- a/src/SixLabors.Fonts/GlyphMetrics.cs +++ b/src/SixLabors.Fonts/GlyphMetrics.cs @@ -18,6 +18,7 @@ public class GlyphMetrics private GlyphVector vector; private readonly Dictionary scaledVector = new(); private Vector2 offset = Vector2.Zero; + private readonly TextRun textRun = new(); internal GlyphMetrics( StreamFontMetrics font, @@ -68,7 +69,7 @@ internal GlyphMetrics( this.GlyphColor = glyphColor; } - internal GlyphMetrics(GlyphMetrics other, CodePoint codePoint) + internal GlyphMetrics(GlyphMetrics other, TextRun textRun, CodePoint codePoint) { this.FontMetrics = other.FontMetrics; this.CodePoint = codePoint; @@ -88,6 +89,7 @@ internal GlyphMetrics(GlyphMetrics other, CodePoint codePoint) this.ScaleFactor = other.ScaleFactor; this.GlyphColor = other.GlyphColor; this.offset = other.offset; + this.textRun = textRun; } /// @@ -228,7 +230,7 @@ public void RenderTo(IGlyphRenderer surface, float pointSize, Vector2 location, FontRectangle box = this.GetBoundingBox(location, scaledPoint); - var parameters = new GlyphRendererParameters(this, pointSize, dpi); + var parameters = new GlyphRendererParameters(this, this.textRun, pointSize, dpi); if (surface.BeginGlyph(box, parameters)) { @@ -239,6 +241,7 @@ public void RenderTo(IGlyphRenderer surface, float pointSize, Vector2 location, if (!this.scaledVector.TryGetValue(scaledPoint, out GlyphVector scaledVector)) { + // TODO: Optimize to a single transform. Vector2 scale = new Vector2(scaledPoint) / this.ScaleFactor; scaledVector = GlyphVector.Scale(this.vector, scale); diff --git a/src/SixLabors.Fonts/GlyphPositioningCollection.cs b/src/SixLabors.Fonts/GlyphPositioningCollection.cs index 519c74cb9..6fc1c68db 100644 --- a/src/SixLabors.Fonts/GlyphPositioningCollection.cs +++ b/src/SixLabors.Fonts/GlyphPositioningCollection.cs @@ -130,7 +130,7 @@ public bool TryUpdate(Font font, GlyphSubstitutionCollection collection) CodePoint codePoint = data.CodePoint; ushort[] glyphIds = data.GlyphIds; - TextAttribute textAttributes = data.TextAttributes; + TextAttribute textAttributes = data.TextRun.TextAttributes; var m = new List(glyphIds.Length); foreach (ushort id in glyphIds) @@ -147,7 +147,9 @@ public bool TryUpdate(Font font, GlyphSubstitutionCollection collection) break; } - m.Add(new GlyphMetrics(gm, codePoint)); + // We slip the text run in here while we clone so we have + // it available to the renderer. + m.Add(new GlyphMetrics(gm, data.TextRun, codePoint)); } } @@ -189,7 +191,7 @@ public bool TryAdd(Font font, GlyphSubstitutionCollection collection) GlyphShapingData data = collection.GetGlyphShapingData(i, out int offset); CodePoint codePoint = data.CodePoint; ushort[] glyphIds = data.GlyphIds; - TextAttribute textAttributes = data.TextAttributes; + TextAttribute textAttributes = data.TextRun.TextAttributes; var m = new List(glyphIds.Length); foreach (ushort id in glyphIds) @@ -203,7 +205,9 @@ public bool TryAdd(Font font, GlyphSubstitutionCollection collection) hasFallBacks = true; } - m.Add(new GlyphMetrics(gm, codePoint)); + // We slip the text run in here while we clone so we have + // it available to the renderer. + m.Add(new GlyphMetrics(gm, data.TextRun, codePoint)); } } diff --git a/src/SixLabors.Fonts/GlyphRendererParameters.cs b/src/SixLabors.Fonts/GlyphRendererParameters.cs index 62e31ce30..94919accf 100644 --- a/src/SixLabors.Fonts/GlyphRendererParameters.cs +++ b/src/SixLabors.Fonts/GlyphRendererParameters.cs @@ -13,7 +13,7 @@ namespace SixLabors.Fonts [DebuggerDisplay("GlyphIndex = {GlyphIndex}, PointSize = {PointSize}, DpiX = {DpiX}, DpiY = {DpiY}")] public readonly struct GlyphRendererParameters : IEquatable { - internal GlyphRendererParameters(GlyphMetrics glyph, float pointSize, float dpi) + internal GlyphRendererParameters(GlyphMetrics glyph, TextRun textRun, float pointSize, float dpi) { this.Font = glyph.FontMetrics.Description.FontNameInvariantCulture?.ToUpper() ?? string.Empty; this.FontStyle = glyph.FontMetrics.Description.Style; @@ -22,6 +22,7 @@ internal GlyphRendererParameters(GlyphMetrics glyph, float pointSize, float dpi) this.Dpi = dpi; this.GlyphType = glyph.GlyphType; this.GlyphColor = glyph.GlyphColor ?? default; + this.TextRun = textRun; } /// @@ -55,10 +56,15 @@ internal GlyphRendererParameters(GlyphMetrics glyph, float pointSize, float dpi) public float PointSize { get; } /// - /// Gets the dpi along the X axis we are rendering at. + /// Gets the dots-per-inch we are rendering at. /// public float Dpi { get; } + /// + /// Gets the text run this glyph belongs to. + /// + public TextRun TextRun { get; } + /// /// Compares two objects for equality. /// diff --git a/src/SixLabors.Fonts/GlyphShapingData.cs b/src/SixLabors.Fonts/GlyphShapingData.cs index 067e820e1..3019b476d 100644 --- a/src/SixLabors.Fonts/GlyphShapingData.cs +++ b/src/SixLabors.Fonts/GlyphShapingData.cs @@ -18,9 +18,7 @@ internal class GlyphShapingData /// /// Initializes a new instance of the class. /// - public GlyphShapingData() - { - } + public GlyphShapingData(TextRun textRun) => this.TextRun = textRun; /// /// Initializes a new instance of the class. @@ -32,7 +30,7 @@ public GlyphShapingData(GlyphShapingData data, bool clearFeatures = false) this.CodePoint = data.CodePoint; this.CodePointCount = data.CodePointCount; this.Direction = data.Direction; - this.TextAttributes = data.TextAttributes; + this.TextRun = data.TextRun; this.GlyphIds = data.GlyphIds; this.LigatureId = data.LigatureId; this.LigatureComponent = data.LigatureComponent; @@ -63,9 +61,9 @@ public GlyphShapingData(GlyphShapingData data, bool clearFeatures = false) public TextDirection Direction { get; set; } /// - /// Gets or sets the text attributes applied to this glyph. + /// Gets or sets the text run this glyph belongs to. /// - public TextAttribute TextAttributes { get; set; } + public TextRun TextRun { get; set; } /// /// Gets or sets the collection of glyph ids. @@ -104,6 +102,6 @@ public GlyphShapingData(GlyphShapingData data, bool clearFeatures = false) private string DebuggerDisplay => FormattableString - .Invariant($"{this.CodePoint.ToDebuggerDisplay()} : {CodePoint.GetScriptClass(this.CodePoint)} : {this.Direction} : {this.TextAttributes} : {this.LigatureId} : {this.LigatureComponent} : [{string.Join(",", this.GlyphIds)}]"); + .Invariant($"{this.CodePoint.ToDebuggerDisplay()} : {CodePoint.GetScriptClass(this.CodePoint)} : {this.Direction} : {this.TextRun.TextAttributes} : {this.LigatureId} : {this.LigatureComponent} : [{string.Join(",", this.GlyphIds)}]"); } } diff --git a/src/SixLabors.Fonts/GlyphSubstitutionCollection.cs b/src/SixLabors.Fonts/GlyphSubstitutionCollection.cs index 8eee37081..509f0d7e8 100644 --- a/src/SixLabors.Fonts/GlyphSubstitutionCollection.cs +++ b/src/SixLabors.Fonts/GlyphSubstitutionCollection.cs @@ -94,15 +94,14 @@ public void EnableShapingFeature(int index, Tag feature) /// The id of the glyph to add. /// The codepoint the glyph represents. /// The resolved text direction for the codepoint. - /// The text attributes to apply to the glyph. + /// The text run this glyph belongs to. /// The zero-based index within the input codepoint collection. - public void AddGlyph(ushort glyphId, CodePoint codePoint, TextDirection direction, TextAttribute textAttributes, int offset) + public void AddGlyph(ushort glyphId, CodePoint codePoint, TextDirection direction, TextRun textRun, int offset) { - this.glyphs.Add(offset, new() + this.glyphs.Add(offset, new(textRun) { CodePoint = codePoint, Direction = direction, - TextAttributes = textAttributes, GlyphIds = new[] { glyphId }, }); this.offsets.Add(offset); diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/AnchorTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/AnchorTable.cs index cc19761b7..09a2f8304 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/AnchorTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/AnchorTable.cs @@ -111,7 +111,7 @@ public override AnchorXY GetAnchor(FontMetrics fontMetrics, GlyphShapingData dat { if (collection.TextOptions.ApplyHinting) { - foreach (GlyphMetrics metric in fontMetrics.GetGlyphMetrics(data.CodePoint, data.TextAttributes, collection.TextOptions.ColorFontSupport)) + foreach (GlyphMetrics metric in fontMetrics.GetGlyphMetrics(data.CodePoint, data.TextRun.TextAttributes, collection.TextOptions.ColorFontSupport)) { ReadOnlyMemory points = metric.GetOutline().ControlPoints; if (this.anchorPointIndex < points.Length) diff --git a/src/SixLabors.Fonts/TextLayout.cs b/src/SixLabors.Fonts/TextLayout.cs index 49e14e0b3..4dc1cfa77 100644 --- a/src/SixLabors.Fonts/TextLayout.cs +++ b/src/SixLabors.Fonts/TextLayout.cs @@ -526,7 +526,7 @@ private static bool DoFontRun( // Get the glyph id for the codepoint and add to the collection. font.FontMetrics.TryGetGlyphId(current, next, out ushort glyphId, out skipNextCodePoint); - substitutions.AddGlyph(glyphId, current, (TextDirection)bidiRuns[bidiRun].Direction, textRuns[textRun].TextAttributes, codePointIndex); + substitutions.AddGlyph(glyphId, current, (TextDirection)bidiRuns[bidiRun].Direction, textRuns[textRun], codePointIndex); codePointIndex++; graphemeCodePointIndex++; From 3439a981b3d16e88f1d49f826ec559217bd90ca8 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sat, 5 Mar 2022 01:05:07 +1100 Subject: [PATCH 17/48] Optimize scaled transform to a single call --- src/SixLabors.Fonts/GlyphMetrics.cs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/SixLabors.Fonts/GlyphMetrics.cs b/src/SixLabors.Fonts/GlyphMetrics.cs index 974a39c85..a2ec7cee3 100644 --- a/src/SixLabors.Fonts/GlyphMetrics.cs +++ b/src/SixLabors.Fonts/GlyphMetrics.cs @@ -241,13 +241,11 @@ public void RenderTo(IGlyphRenderer surface, float pointSize, Vector2 location, if (!this.scaledVector.TryGetValue(scaledPoint, out GlyphVector scaledVector)) { - // TODO: Optimize to a single transform. + // Scale and translate the glyph Vector2 scale = new Vector2(scaledPoint) / this.ScaleFactor; - scaledVector = GlyphVector.Scale(this.vector, scale); - - Vector2 offset = this.offset * scale * MirrorScale; - scaledVector = GlyphVector.Translate(scaledVector, offset.X, offset.Y); - + var transform = Matrix3x2.CreateScale(scale); + transform.Translation = this.offset * scale * MirrorScale; + scaledVector = GlyphVector.Transform(this.vector, transform); this.scaledVector[scaledPoint] = scaledVector; } From af80bc8cc06878cec22b208500420705fef20843 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sat, 5 Mar 2022 01:48:27 +1100 Subject: [PATCH 18/48] Restore good API --- src/SixLabors.Fonts/FileFontMetrics.cs | 8 +- src/SixLabors.Fonts/Font.cs | 5 +- src/SixLabors.Fonts/FontMetrics.cs | 6 +- src/SixLabors.Fonts/GlyphMetrics.cs | 99 +++++++++---------- .../GlyphPositioningCollection.cs | 14 ++- src/SixLabors.Fonts/StreamFontMetrics.cs | 21 ++-- .../AdvancedTypographic/GPos/AnchorTable.cs | 2 +- .../SixLabors.Fonts.Tests/FontLoaderTests.cs | 12 +-- .../SixLabors.Fonts.Tests/FontMetricsTests.cs | 16 +-- tests/SixLabors.Fonts.Tests/GlyphTests.cs | 4 +- .../Issues/Issues_191.cs | 5 +- .../SixLabors.Fonts.Tests/TextLayoutTests.cs | 2 +- 12 files changed, 92 insertions(+), 102 deletions(-) diff --git a/src/SixLabors.Fonts/FileFontMetrics.cs b/src/SixLabors.Fonts/FileFontMetrics.cs index 63ab45894..8acd655ea 100644 --- a/src/SixLabors.Fonts/FileFontMetrics.cs +++ b/src/SixLabors.Fonts/FileFontMetrics.cs @@ -130,12 +130,12 @@ internal override bool TryGetMarkAttachmentClass(ushort glyphId, [NotNullWhen(tr => this.metrics.Value.TryGetMarkAttachmentClass(glyphId, out markAttachmentClass); /// - public override IEnumerable GetGlyphMetrics(CodePoint codePoint, TextAttribute textAttributes, ColorFontSupport support) - => this.metrics.Value.GetGlyphMetrics(codePoint, textAttributes, support); + public override IEnumerable GetGlyphMetrics(CodePoint codePoint, ColorFontSupport support) + => this.metrics.Value.GetGlyphMetrics(codePoint, support); /// - internal override IEnumerable GetGlyphMetrics(CodePoint codePoint, ushort glyphId, TextAttribute textAttributes, ColorFontSupport support) - => this.metrics.Value.GetGlyphMetrics(codePoint, glyphId, textAttributes, support); + internal override IEnumerable GetGlyphMetrics(CodePoint codePoint, ushort glyphId, ColorFontSupport support) + => this.metrics.Value.GetGlyphMetrics(codePoint, glyphId, support); /// internal override void ApplySubstitution(GlyphSubstitutionCollection collection) diff --git a/src/SixLabors.Fonts/Font.cs b/src/SixLabors.Fonts/Font.cs index 6b118047b..0c0387fe0 100644 --- a/src/SixLabors.Fonts/Font.cs +++ b/src/SixLabors.Fonts/Font.cs @@ -146,12 +146,11 @@ public bool TryGetPath([NotNullWhen(true)] out string? path) /// Gets the glyphs for the given codepoint. /// /// The code point of the character. - /// The text attributes to assign to the glyph. /// Options for enabling color font support during layout and rendering. /// Returns the glyph - public IEnumerable GetGlyphs(CodePoint codePoint, TextAttribute textAttributes, ColorFontSupport support) + public IEnumerable GetGlyphs(CodePoint codePoint, ColorFontSupport support) { - foreach (GlyphMetrics metrics in this.FontMetrics.GetGlyphMetrics(codePoint, textAttributes, support)) + foreach (GlyphMetrics metrics in this.FontMetrics.GetGlyphMetrics(codePoint, support)) { yield return new(metrics, this.Size); } diff --git a/src/SixLabors.Fonts/FontMetrics.cs b/src/SixLabors.Fonts/FontMetrics.cs index 72d706ee7..f473c25d9 100644 --- a/src/SixLabors.Fonts/FontMetrics.cs +++ b/src/SixLabors.Fonts/FontMetrics.cs @@ -187,10 +187,9 @@ internal FontMetrics() /// Gets the glyph metrics for a given code point. /// /// The Unicode code point to get the glyph for. - /// The text attributes to assign to the glyph. /// Options for enabling color font support during layout and rendering. /// The glyph metrics to find. - public abstract IEnumerable GetGlyphMetrics(CodePoint codePoint, TextAttribute textAttributes, ColorFontSupport support); + public abstract IEnumerable GetGlyphMetrics(CodePoint codePoint, ColorFontSupport support); /// /// Gets the glyph metrics for a given code point and glyph id. @@ -200,10 +199,9 @@ internal FontMetrics() /// The previously matched or substituted glyph id for the codepoint in the face. /// If this value equals 0 the default fallback metrics are returned. /// - /// The text attributes to assigned to the glyph. /// Options for enabling color font support during layout and rendering. /// The . - internal abstract IEnumerable GetGlyphMetrics(CodePoint codePoint, ushort glyphId, TextAttribute textAttributes, ColorFontSupport support); + internal abstract IEnumerable GetGlyphMetrics(CodePoint codePoint, ushort glyphId, ColorFontSupport support); /// /// Applies any available substitutions to the collection of glyphs. diff --git a/src/SixLabors.Fonts/GlyphMetrics.cs b/src/SixLabors.Fonts/GlyphMetrics.cs index a2ec7cee3..ff0ead98a 100644 --- a/src/SixLabors.Fonts/GlyphMetrics.cs +++ b/src/SixLabors.Fonts/GlyphMetrics.cs @@ -18,7 +18,7 @@ public class GlyphMetrics private GlyphVector vector; private readonly Dictionary scaledVector = new(); private Vector2 offset = Vector2.Zero; - private readonly TextRun textRun = new(); + private TextRun textRun = new(); internal GlyphMetrics( StreamFontMetrics font, @@ -31,7 +31,6 @@ internal GlyphMetrics( ushort unitsPerEM, ushort glyphId, GlyphType glyphType = GlyphType.Standard, - TextAttribute textAttributes = TextAttribute.None, GlyphColor? glyphColor = null) { this.FontMetrics = font; @@ -47,51 +46,12 @@ internal GlyphMetrics( this.Width = bounds.Max.X - bounds.Min.X; this.Height = bounds.Max.Y - bounds.Min.Y; this.GlyphType = glyphType; - this.TextAttributes = textAttributes; this.LeftSideBearing = leftSideBearing; this.TopSideBearing = topSideBearing; - - Vector2 scaleFactor = new(unitsPerEM * 72F); - if (this.TextAttributes.HasFlag(TextAttribute.Subscript)) - { - float units = unitsPerEM; - scaleFactor /= new Vector2(this.FontMetrics.SubscriptXSize / units, this.FontMetrics.SubscriptYSize / units); - this.offset = new(this.FontMetrics.SubscriptXOffset, this.FontMetrics.SubscriptYOffset); - } - else if (this.TextAttributes.HasFlag(TextAttribute.Superscript)) - { - float units = unitsPerEM; - scaleFactor /= new Vector2(this.FontMetrics.SuperscriptXSize / units, this.FontMetrics.SuperscriptYSize / units); - this.offset = new(this.FontMetrics.SuperscriptXOffset, -this.FontMetrics.SuperscriptYOffset); - } - - this.ScaleFactor = scaleFactor; + this.ScaleFactor = new(unitsPerEM * 72F); this.GlyphColor = glyphColor; } - internal GlyphMetrics(GlyphMetrics other, TextRun textRun, CodePoint codePoint) - { - this.FontMetrics = other.FontMetrics; - this.CodePoint = codePoint; - this.UnitsPerEm = other.UnitsPerEm; - this.vector = GlyphVector.DeepClone(other.vector); - - this.AdvanceWidth = other.AdvanceWidth; - this.AdvanceHeight = other.AdvanceHeight; - this.GlyphId = other.GlyphId; - - this.Width = other.Width; - this.Height = other.Height; - this.GlyphType = other.GlyphType; - this.TextAttributes = other.TextAttributes; - this.LeftSideBearing = other.LeftSideBearing; - this.TopSideBearing = other.TopSideBearing; - this.ScaleFactor = other.ScaleFactor; - this.GlyphColor = other.GlyphColor; - this.offset = other.offset; - this.textRun = textRun; - } - /// /// Gets the font metrics. /// @@ -137,11 +97,6 @@ internal GlyphMetrics(GlyphMetrics other, TextRun textRun, CodePoint codePoint) /// public GlyphType GlyphType { get; } - /// - /// Gets the text attributes applied to this glyph. - /// - public TextAttribute TextAttributes { get; } - /// /// Gets the color of this glyph when the is /// @@ -151,13 +106,59 @@ internal GlyphMetrics(GlyphMetrics other, TextRun textRun, CodePoint codePoint) public ushort UnitsPerEm { get; } /// - public Vector2 ScaleFactor { get; } + public Vector2 ScaleFactor { get; private set; } /// /// Gets the glyph Id. /// internal ushort GlyphId { get; } + /// + /// Performs a semi-deep clone (FontMetrics are not cloned) for rendering + /// This allows caching the original in the font metrics. + /// + /// The original glyph metrics. + /// The text run this glyph is a member of. + /// The codepoint for this glyph. + /// The new . + internal static GlyphMetrics CloneForRendering(GlyphMetrics other, TextRun textRun, CodePoint codePoint) + { + StreamFontMetrics fontMetrics = other.FontMetrics; + Vector2 offset = other.offset; + Vector2 scaleFactor = other.ScaleFactor; + if (textRun.TextAttributes.HasFlag(TextAttribute.Subscript)) + { + float units = other.UnitsPerEm; + scaleFactor /= new Vector2(fontMetrics.SubscriptXSize / units, fontMetrics.SubscriptYSize / units); + offset = new(other.FontMetrics.SubscriptXOffset, other.FontMetrics.SubscriptYOffset); + } + else if (textRun.TextAttributes.HasFlag(TextAttribute.Superscript)) + { + float units = other.UnitsPerEm; + scaleFactor /= new Vector2(fontMetrics.SuperscriptXSize / units, fontMetrics.SuperscriptYSize / units); + offset = new(fontMetrics.SuperscriptXOffset, -fontMetrics.SuperscriptYOffset); + } + + GlyphMetrics metrics = new( + fontMetrics, + codePoint, + GlyphVector.DeepClone(other.vector), + other.AdvanceWidth, + other.AdvanceHeight, + other.LeftSideBearing, + other.TopSideBearing, + other.UnitsPerEm, + other.GlyphId, + other.GlyphType, + other.GlyphColor); + + metrics.offset = offset; + metrics.ScaleFactor = scaleFactor; + metrics.textRun = textRun; + + return metrics; + } + /// /// Gets the outline for the current glyph. /// @@ -326,8 +327,6 @@ public void RenderTo(IGlyphRenderer surface, float pointSize, Vector2 location, surface.EndFigure(); } - - // TODO: Draw rectangles for strikethrough, underline. } surface.EndGlyph(); diff --git a/src/SixLabors.Fonts/GlyphPositioningCollection.cs b/src/SixLabors.Fonts/GlyphPositioningCollection.cs index 6fc1c68db..c3ab20b49 100644 --- a/src/SixLabors.Fonts/GlyphPositioningCollection.cs +++ b/src/SixLabors.Fonts/GlyphPositioningCollection.cs @@ -130,14 +130,13 @@ public bool TryUpdate(Font font, GlyphSubstitutionCollection collection) CodePoint codePoint = data.CodePoint; ushort[] glyphIds = data.GlyphIds; - TextAttribute textAttributes = data.TextRun.TextAttributes; - var m = new List(glyphIds.Length); + foreach (ushort id in glyphIds) { // Perform a semi-deep clone (FontMetrics is not cloned) so we can continue to // cache the original in the font metrics and only update our collection. - foreach (GlyphMetrics gm in fontMetrics.GetGlyphMetrics(codePoint, id, textAttributes, colorFontSupport)) + foreach (GlyphMetrics gm in fontMetrics.GetGlyphMetrics(codePoint, id, colorFontSupport)) { if (gm.GlyphType == GlyphType.Fallback && !CodePoint.IsControl(codePoint)) { @@ -149,7 +148,7 @@ public bool TryUpdate(Font font, GlyphSubstitutionCollection collection) // We slip the text run in here while we clone so we have // it available to the renderer. - m.Add(new GlyphMetrics(gm, data.TextRun, codePoint)); + m.Add(GlyphMetrics.CloneForRendering(gm, data.TextRun, codePoint)); } } @@ -191,14 +190,13 @@ public bool TryAdd(Font font, GlyphSubstitutionCollection collection) GlyphShapingData data = collection.GetGlyphShapingData(i, out int offset); CodePoint codePoint = data.CodePoint; ushort[] glyphIds = data.GlyphIds; - TextAttribute textAttributes = data.TextRun.TextAttributes; - var m = new List(glyphIds.Length); + foreach (ushort id in glyphIds) { // Perform a semi-deep clone (FontMetrics is not cloned) so we can continue to // cache the original in the font metrics and only update our collection. - foreach (GlyphMetrics gm in fontMetrics.GetGlyphMetrics(codePoint, id, textAttributes, colorFontSupport)) + foreach (GlyphMetrics gm in fontMetrics.GetGlyphMetrics(codePoint, id, colorFontSupport)) { if (gm.GlyphType == GlyphType.Fallback && !CodePoint.IsControl(codePoint)) { @@ -207,7 +205,7 @@ public bool TryAdd(Font font, GlyphSubstitutionCollection collection) // We slip the text run in here while we clone so we have // it available to the renderer. - m.Add(new GlyphMetrics(gm, data.TextRun, codePoint)); + m.Add(GlyphMetrics.CloneForRendering(gm, data.TextRun, codePoint)); } } diff --git a/src/SixLabors.Fonts/StreamFontMetrics.cs b/src/SixLabors.Fonts/StreamFontMetrics.cs index e1b4e263d..5262efbfb 100644 --- a/src/SixLabors.Fonts/StreamFontMetrics.cs +++ b/src/SixLabors.Fonts/StreamFontMetrics.cs @@ -271,14 +271,14 @@ internal override bool TryGetMarkAttachmentClass(ushort glyphId, [NotNullWhen(tr } /// - public override IEnumerable GetGlyphMetrics(CodePoint codePoint, TextAttribute textAttributes, ColorFontSupport support) + public override IEnumerable GetGlyphMetrics(CodePoint codePoint, ColorFontSupport support) { this.TryGetGlyphId(codePoint, out ushort glyphId); - return this.GetGlyphMetrics(codePoint, glyphId, textAttributes, support); + return this.GetGlyphMetrics(codePoint, glyphId, support); } /// - internal override IEnumerable GetGlyphMetrics(CodePoint codePoint, ushort glyphId, TextAttribute textAttributes, ColorFontSupport support) + internal override IEnumerable GetGlyphMetrics(CodePoint codePoint, ushort glyphId, ColorFontSupport support) { GlyphType glyphType = GlyphType.Standard; if (glyphId == 0) @@ -289,22 +289,21 @@ internal override IEnumerable GetGlyphMetrics(CodePoint codePoint, } if (support == ColorFontSupport.MicrosoftColrFormat - && this.TryGetColoredMetrics(codePoint, glyphId, textAttributes, out GlyphMetrics[]? metrics)) + && this.TryGetColoredMetrics(codePoint, glyphId, out GlyphMetrics[]? metrics)) { return metrics; } // We overwrite the cache entry for this type should the attributes change. GlyphMetrics[]? cached = this.glyphCache[glyphId]; - if (cached == null || cached[0]!.TextAttributes != textAttributes) + if (cached is null) { this.glyphCache[glyphId] = new[] { this.CreateGlyphMetrics( codePoint, glyphId, - glyphType, - textAttributes) + glyphType) }; } @@ -512,7 +511,7 @@ internal GlyphVector ApplyHinting(GlyphVector glyphVector, float pixelSize, usho return glyphVector; } - private bool TryGetColoredMetrics(CodePoint codePoint, ushort glyphId, TextAttribute textAttributes, [NotNullWhen(true)] out GlyphMetrics[]? metrics) + private bool TryGetColoredMetrics(CodePoint codePoint, ushort glyphId, [NotNullWhen(true)] out GlyphMetrics[]? metrics) { if (this.colrTable == null || this.colorGlyphCache == null) { @@ -522,7 +521,7 @@ private bool TryGetColoredMetrics(CodePoint codePoint, ushort glyphId, TextAttri // We overwrite the cache entry for this type should the attributes change. metrics = this.colorGlyphCache[glyphId]; - if (metrics == null || metrics[0].TextAttributes != textAttributes) + if (metrics is null) { Span indexes = this.colrTable.GetLayers(glyphId); if (indexes.Length > 0) @@ -532,7 +531,7 @@ private bool TryGetColoredMetrics(CodePoint codePoint, ushort glyphId, TextAttri { LayerRecord? layer = indexes[i]; - metrics[i] = this.CreateGlyphMetrics(codePoint, layer.GlyphId, GlyphType.ColrLayer, textAttributes, layer.PaletteIndex); + metrics[i] = this.CreateGlyphMetrics(codePoint, layer.GlyphId, GlyphType.ColrLayer, layer.PaletteIndex); } } @@ -547,7 +546,6 @@ private GlyphMetrics CreateGlyphMetrics( CodePoint codePoint, ushort glyphId, GlyphType glyphType, - TextAttribute textAttributes, ushort palleteIndex = 0) { GlyphVector vector = this.glyphs.GetGlyph(glyphId); @@ -585,7 +583,6 @@ private GlyphMetrics CreateGlyphMetrics( this.UnitsPerEm, glyphId, glyphType, - textAttributes, color); } } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/AnchorTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/AnchorTable.cs index 09a2f8304..ca14fdf22 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/AnchorTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/AnchorTable.cs @@ -111,7 +111,7 @@ public override AnchorXY GetAnchor(FontMetrics fontMetrics, GlyphShapingData dat { if (collection.TextOptions.ApplyHinting) { - foreach (GlyphMetrics metric in fontMetrics.GetGlyphMetrics(data.CodePoint, data.TextRun.TextAttributes, collection.TextOptions.ColorFontSupport)) + foreach (GlyphMetrics metric in fontMetrics.GetGlyphMetrics(data.CodePoint, collection.TextOptions.ColorFontSupport)) { ReadOnlyMemory points = metric.GetOutline().ControlPoints; if (this.anchorPointIndex < points.Length) diff --git a/tests/SixLabors.Fonts.Tests/FontLoaderTests.cs b/tests/SixLabors.Fonts.Tests/FontLoaderTests.cs index 031915044..6f126bb0f 100644 --- a/tests/SixLabors.Fonts.Tests/FontLoaderTests.cs +++ b/tests/SixLabors.Fonts.Tests/FontLoaderTests.cs @@ -13,7 +13,7 @@ public class FontLoaderTests public void Issue21_LoopDetectedLoadingGlyphs() { Font font = new FontCollection().Add(TestFonts.CarterOneFileData()).CreateFont(12); - font.FontMetrics.GetGlyphMetrics(new CodePoint('\0'), TextAttribute.None, ColorFontSupport.None).First(); + font.FontMetrics.GetGlyphMetrics(new CodePoint('\0'), ColorFontSupport.None).First(); } [Fact] @@ -39,7 +39,7 @@ public void LoadFont_WithTtfFormat() { Font font = new FontCollection().Add(TestFonts.OpenSansFile).CreateFont(12); - GlyphMetrics glyph = font.FontMetrics.GetGlyphMetrics(new CodePoint('A'), TextAttribute.None, ColorFontSupport.None).First(); + GlyphMetrics glyph = font.FontMetrics.GetGlyphMetrics(new CodePoint('A'), ColorFontSupport.None).First(); var r = new GlyphRenderer(); glyph.RenderTo(r, font.Size, System.Numerics.Vector2.Zero, new TextOptions(font)); @@ -53,7 +53,7 @@ public void LoadFont_WithWoff1Format() { Font font = new FontCollection().Add(TestFonts.OpenSansFileWoff1).CreateFont(12); - GlyphMetrics glyph = font.FontMetrics.GetGlyphMetrics(new CodePoint('A'), TextAttribute.None, ColorFontSupport.None).First(); + GlyphMetrics glyph = font.FontMetrics.GetGlyphMetrics(new CodePoint('A'), ColorFontSupport.None).First(); var r = new GlyphRenderer(); glyph.RenderTo(r, font.Size, System.Numerics.Vector2.Zero, new TextOptions(font)); @@ -86,7 +86,7 @@ public void LoadFont_WithWoff2Format() { Font font = new FontCollection().Add(TestFonts.OpensSansWoff2Data()).CreateFont(12); - GlyphMetrics glyph = font.FontMetrics.GetGlyphMetrics(new CodePoint('A'), TextAttribute.None, ColorFontSupport.None).First(); + GlyphMetrics glyph = font.FontMetrics.GetGlyphMetrics(new CodePoint('A'), ColorFontSupport.None).First(); var r = new GlyphRenderer(); glyph.RenderTo(r, font.Size, System.Numerics.Vector2.Zero, new TextOptions(font)); @@ -104,7 +104,7 @@ public void LoadFont() Assert.Equal("SixLaborsSampleAB regular", font.FontMetrics.Description.FontNameInvariantCulture); Assert.Equal("Regular", font.FontMetrics.Description.FontSubFamilyNameInvariantCulture); - GlyphMetrics glyph = font.FontMetrics.GetGlyphMetrics(new CodePoint('a'), TextAttribute.None, ColorFontSupport.None).First(); + GlyphMetrics glyph = font.FontMetrics.GetGlyphMetrics(new CodePoint('a'), ColorFontSupport.None).First(); var r = new GlyphRenderer(); glyph.RenderTo(r, font.Size, System.Numerics.Vector2.Zero, new TextOptions(font)); @@ -120,7 +120,7 @@ public void LoadFontWoff() Assert.Equal("SixLaborsSampleAB regular", font.FontMetrics.Description.FontNameInvariantCulture); Assert.Equal("Regular", font.FontMetrics.Description.FontSubFamilyNameInvariantCulture); - GlyphMetrics glyph = font.FontMetrics.GetGlyphMetrics(new CodePoint('a'), TextAttribute.None, ColorFontSupport.None).First(); + GlyphMetrics glyph = font.FontMetrics.GetGlyphMetrics(new CodePoint('a'), ColorFontSupport.None).First(); var r = new GlyphRenderer(); glyph.RenderTo(r, 12, System.Numerics.Vector2.Zero, new TextOptions(font)); diff --git a/tests/SixLabors.Fonts.Tests/FontMetricsTests.cs b/tests/SixLabors.Fonts.Tests/FontMetricsTests.cs index e0ac4e93b..f28cd3a2f 100644 --- a/tests/SixLabors.Fonts.Tests/FontMetricsTests.cs +++ b/tests/SixLabors.Fonts.Tests/FontMetricsTests.cs @@ -60,8 +60,8 @@ public void GlyphMetricsMatchesReference() Font font = family.CreateFont(12); var codePoint = new CodePoint('A'); - GlyphMetrics glyphMetrics = font.FontMetrics.GetGlyphMetrics(codePoint, TextAttribute.None, ColorFontSupport.None).First(); - GlyphMetrics glyphMetrics1 = font.GetGlyphs(codePoint, TextAttribute.None, ColorFontSupport.None).First().GlyphMetrics; + GlyphMetrics glyphMetrics = font.FontMetrics.GetGlyphMetrics(codePoint, ColorFontSupport.None).First(); + GlyphMetrics glyphMetrics1 = font.GetGlyphs(codePoint, ColorFontSupport.None).First().GlyphMetrics; Assert.Equal(glyphMetrics, glyphMetrics1); @@ -87,8 +87,8 @@ public void GlyphMetricsMatchesReference_WithWoff1format() Font font = family.CreateFont(12); var codePoint = new CodePoint('A'); - GlyphMetrics glyphMetrics = font.FontMetrics.GetGlyphMetrics(codePoint, TextAttribute.None, ColorFontSupport.None).First(); - GlyphMetrics glyphMetrics1 = font.GetGlyphs(codePoint, TextAttribute.None, ColorFontSupport.None).First().GlyphMetrics; + GlyphMetrics glyphMetrics = font.FontMetrics.GetGlyphMetrics(codePoint, ColorFontSupport.None).First(); + GlyphMetrics glyphMetrics1 = font.GetGlyphs(codePoint, ColorFontSupport.None).First().GlyphMetrics; Assert.Equal(glyphMetrics, glyphMetrics1); @@ -115,8 +115,8 @@ public void GlyphMetricsMatchesReference_WithWoff2format() Font font = family.CreateFont(12); var codePoint = new CodePoint('A'); - GlyphMetrics glyphMetrics = font.FontMetrics.GetGlyphMetrics(codePoint, TextAttribute.None, ColorFontSupport.None).First(); - GlyphMetrics glyphMetrics1 = font.GetGlyphs(codePoint, TextAttribute.None, ColorFontSupport.None).First().GlyphMetrics; + GlyphMetrics glyphMetrics = font.FontMetrics.GetGlyphMetrics(codePoint, ColorFontSupport.None).First(); + GlyphMetrics glyphMetrics1 = font.GetGlyphs(codePoint, ColorFontSupport.None).First().GlyphMetrics; Assert.Equal(glyphMetrics, glyphMetrics1); @@ -143,8 +143,8 @@ public void GlyphMetricsVerticalMatchesReference() Font font = family.CreateFont(12); var codePoint = new CodePoint('A'); - GlyphMetrics glyphMetrics = font.FontMetrics.GetGlyphMetrics(codePoint, TextAttribute.None, ColorFontSupport.None).First(); - GlyphMetrics glyphMetrics1 = font.GetGlyphs(codePoint, TextAttribute.None, ColorFontSupport.None).First().GlyphMetrics; + GlyphMetrics glyphMetrics = font.FontMetrics.GetGlyphMetrics(codePoint, ColorFontSupport.None).First(); + GlyphMetrics glyphMetrics1 = font.GetGlyphs(codePoint, ColorFontSupport.None).First().GlyphMetrics; Assert.Equal(glyphMetrics, glyphMetrics1); diff --git a/tests/SixLabors.Fonts.Tests/GlyphTests.cs b/tests/SixLabors.Fonts.Tests/GlyphTests.cs index 9442ba969..1b842e20c 100644 --- a/tests/SixLabors.Fonts.Tests/GlyphTests.cs +++ b/tests/SixLabors.Fonts.Tests/GlyphTests.cs @@ -93,7 +93,7 @@ public void LoadGlyph() Font font = new FontCollection().Add(TestFonts.SimpleFontFileData()).CreateFont(12); // Get letter A - Glyph g = font.GetGlyphs(new CodePoint(41), TextAttribute.None, ColorFontSupport.None).First(); + Glyph g = font.GetGlyphs(new CodePoint(41), ColorFontSupport.None).First(); GlyphOutline instance = g.GlyphMetrics.GetOutline(); Assert.Equal(20, instance.ControlPoints.Length); @@ -109,7 +109,7 @@ public void RenderColrGlyph() var instance = font.FontMetrics as StreamFontMetrics; CodePoint codePoint = this.AsCodePoint("😀"); Assert.True(instance.TryGetGlyphId(codePoint, out ushort idx)); - IEnumerable vectors = instance.GetGlyphMetrics(codePoint, idx, TextAttribute.None, ColorFontSupport.MicrosoftColrFormat); + IEnumerable vectors = instance.GetGlyphMetrics(codePoint, idx, ColorFontSupport.MicrosoftColrFormat); Assert.Equal(3, vectors.Count()); } diff --git a/tests/SixLabors.Fonts.Tests/Issues/Issues_191.cs b/tests/SixLabors.Fonts.Tests/Issues/Issues_191.cs index 8a86249dc..3dbe7d64c 100644 --- a/tests/SixLabors.Fonts.Tests/Issues/Issues_191.cs +++ b/tests/SixLabors.Fonts.Tests/Issues/Issues_191.cs @@ -17,10 +17,9 @@ public void CanLoadMacintoshGlyphs() .First(x => x.GetAvailableStyles().Contains(FontStyle.Regular)).CreateFont(12); const ColorFontSupport support = ColorFontSupport.None; - const TextAttribute attributes = TextAttribute.None; - Glyph[] a = font.GetGlyphs(new CodePoint('A'), attributes, support).ToArray(); - Glyph[] x = font.GetGlyphs(new CodePoint('x'), attributes, support).ToArray(); + Glyph[] a = font.GetGlyphs(new CodePoint('A'), support).ToArray(); + Glyph[] x = font.GetGlyphs(new CodePoint('x'), support).ToArray(); Glyph ga = Assert.Single(a); Glyph gx = Assert.Single(x); diff --git a/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs b/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs index 428b8a2eb..1ceb6b544 100644 --- a/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs +++ b/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs @@ -18,7 +18,7 @@ public class TextLayoutTests public void FakeFontGetGlyph() { Font font = CreateFont("hello world"); - Glyph glyph = font.GetGlyphs(new CodePoint('h'), TextAttribute.None, ColorFontSupport.None).First(); + Glyph glyph = font.GetGlyphs(new CodePoint('h'), ColorFontSupport.None).First(); Assert.NotEqual(default, glyph); } From a51f4fa6530e14d3b818cc658a9c12ac6e6482a1 Mon Sep 17 00:00:00 2001 From: Scott Williams Date: Fri, 4 Mar 2022 19:03:07 +0000 Subject: [PATCH 19/48] Render underlines/strikeout parts. --- src/SixLabors.Fonts/GlyphMetrics.cs | 180 +++++++++++------- .../GlyphRendererParameters.cs | 4 +- src/SixLabors.Fonts/StreamFontMetrics.cs | 2 +- src/SixLabors.Fonts/TextRenderer.cs | 8 +- 4 files changed, 119 insertions(+), 75 deletions(-) diff --git a/src/SixLabors.Fonts/GlyphMetrics.cs b/src/SixLabors.Fonts/GlyphMetrics.cs index ff0ead98a..6a6d32a3d 100644 --- a/src/SixLabors.Fonts/GlyphMetrics.cs +++ b/src/SixLabors.Fonts/GlyphMetrics.cs @@ -235,98 +235,140 @@ public void RenderTo(IGlyphRenderer surface, float pointSize, Vector2 location, if (surface.BeginGlyph(box, parameters)) { - if (this.GlyphColor.HasValue && surface is IColorGlyphRenderer colorSurface) + if (!CodePoint.IsWhiteSpace(this.CodePoint)) { - colorSurface.SetColor(this.GlyphColor.Value); - } - - if (!this.scaledVector.TryGetValue(scaledPoint, out GlyphVector scaledVector)) - { - // Scale and translate the glyph - Vector2 scale = new Vector2(scaledPoint) / this.ScaleFactor; - var transform = Matrix3x2.CreateScale(scale); - transform.Translation = this.offset * scale * MirrorScale; - scaledVector = GlyphVector.Transform(this.vector, transform); - this.scaledVector[scaledPoint] = scaledVector; - } - - if (options.ApplyHinting) - { - this.FontMetrics.ApplyHinting(scaledVector, pointSize * dpi / 72, this.GlyphId); - } - - GlyphOutline outline = scaledVector.GetOutline(); - ReadOnlySpan controlPoints = outline.ControlPoints.Span; - ReadOnlySpan endPoints = outline.EndPoints.Span; - ReadOnlySpan onCurves = outline.OnCurves.Span; - - int endOfContour = -1; - for (int i = 0; i < outline.EndPoints.Length; i++) - { - surface.BeginFigure(); - int startOfContour = endOfContour + 1; - endOfContour = endPoints[i]; - - Vector2 prev; - Vector2 curr = (MirrorScale * controlPoints[endOfContour]) + location; - Vector2 next = (MirrorScale * controlPoints[startOfContour]) + location; + if (this.GlyphColor.HasValue && surface is IColorGlyphRenderer colorSurface) + { + colorSurface.SetColor(this.GlyphColor.Value); + } - if (onCurves[endOfContour]) + if (!this.scaledVector.TryGetValue(scaledPoint, out GlyphVector scaledVector)) { - surface.MoveTo(curr); + // Scale and translate the glyph + Vector2 scale = new Vector2(scaledPoint) / this.ScaleFactor; + var transform = Matrix3x2.CreateScale(scale); + transform.Translation = this.offset * scale * MirrorScale; + scaledVector = GlyphVector.Transform(this.vector, transform); + this.scaledVector[scaledPoint] = scaledVector; } - else + + if (options.ApplyHinting) { - if (onCurves[startOfContour]) - { - surface.MoveTo(next); - } - else - { - // If both first and last points are off-curve, start at their middle. - Vector2 startPoint = (curr + next) / 2; - surface.MoveTo(startPoint); - } + this.FontMetrics.ApplyHinting(scaledVector, pointSize * dpi / 72, this.GlyphId); } - int length = endOfContour - startOfContour + 1; - for (int p = 0; p < length; p++) + GlyphOutline outline = scaledVector.GetOutline(); + ReadOnlySpan controlPoints = outline.ControlPoints.Span; + ReadOnlySpan endPoints = outline.EndPoints.Span; + ReadOnlySpan onCurves = outline.OnCurves.Span; + + int endOfContour = -1; + for (int i = 0; i < outline.EndPoints.Length; i++) { - prev = curr; - curr = next; - int currentIndex = startOfContour + p; - int nextIndex = startOfContour + ((p + 1) % length); - int prevIndex = startOfContour + ((length + p - 1) % length); - next = (MirrorScale * controlPoints[nextIndex]) + location; - - if (onCurves[currentIndex]) + surface.BeginFigure(); + int startOfContour = endOfContour + 1; + endOfContour = endPoints[i]; + + Vector2 prev; + Vector2 curr = (MirrorScale * controlPoints[endOfContour]) + location; + Vector2 next = (MirrorScale * controlPoints[startOfContour]) + location; + + if (onCurves[endOfContour]) { - // This is a straight line. - surface.LineTo(curr); + surface.MoveTo(curr); } else { - Vector2 prev2 = prev; - Vector2 next2 = next; - - if (!onCurves[prevIndex]) + if (onCurves[startOfContour]) { - prev2 = (curr + prev) / 2; - surface.LineTo(prev2); + surface.MoveTo(next); } + else + { + // If both first and last points are off-curve, start at their middle. + Vector2 startPoint = (curr + next) / 2; + surface.MoveTo(startPoint); + } + } - if (!onCurves[nextIndex]) + 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 = (MirrorScale * controlPoints[nextIndex]) + location; + + if (onCurves[currentIndex]) { - next2 = (curr + next) / 2; + // This is a straight line. + surface.LineTo(curr); } + else + { + Vector2 prev2 = prev; + Vector2 next2 = next; + + if (!onCurves[prevIndex]) + { + prev2 = (curr + prev) / 2; + surface.LineTo(prev2); + } + + if (!onCurves[nextIndex]) + { + next2 = (curr + next) / 2; + } - surface.LineTo(prev2); - surface.QuadraticBezierTo(curr, next2); + surface.LineTo(prev2); + surface.QuadraticBezierTo(curr, next2); + } } + + surface.EndFigure(); } + } + + void DrawLine(float thickness, float position) + { + surface.BeginFigure(); + + var height = thickness; + var top = position - (height / 2); + var bottom = top + height; + + Vector2 scale = new Vector2(scaledPoint) / this.ScaleFactor * MirrorScale; + + var tl = (new Vector2(0, top) * scale) + location; + var tr = (new Vector2(this.AdvanceWidth, top) * scale) + location; + var br = (new Vector2(this.AdvanceWidth, bottom) * scale) + location; + var bl = (new Vector2(0, bottom) * scale) + location; + + tl.Y = MathF.Ceiling(tl.Y); + tr.Y = MathF.Ceiling(tr.Y); + br.Y = MathF.Ceiling(br.Y); + bl.Y = MathF.Ceiling(bl.Y); + + surface.MoveTo(tl); + surface.LineTo(bl); + surface.LineTo(br); + surface.LineTo(tr); surface.EndFigure(); } + + // lets figure out underline + if ((this.textRun.TextAttributes & TextAttribute.Underline) == TextAttribute.Underline) + { + DrawLine(this.FontMetrics.UnderlineThickness, this.FontMetrics.UnderlinePosition); + } + + if ((this.textRun.TextAttributes & TextAttribute.Strikethrough) == TextAttribute.Strikethrough) + { + DrawLine(this.FontMetrics.StrikeoutSize, this.FontMetrics.StrikeoutPosition); + } } surface.EndGlyph(); diff --git a/src/SixLabors.Fonts/GlyphRendererParameters.cs b/src/SixLabors.Fonts/GlyphRendererParameters.cs index 94919accf..6f54ac74d 100644 --- a/src/SixLabors.Fonts/GlyphRendererParameters.cs +++ b/src/SixLabors.Fonts/GlyphRendererParameters.cs @@ -102,6 +102,7 @@ public bool Equals(GlyphRendererParameters other) && other.Dpi == this.Dpi && other.GlyphIndex == this.GlyphIndex && other.GlyphType == this.GlyphType + && other.TextRun == this.TextRun && other.GlyphColor.Equals(this.GlyphColor) && ((other.Font is null && this.Font is null) || (other.Font?.Equals(this.Font, StringComparison.OrdinalIgnoreCase) == true)); @@ -118,6 +119,7 @@ public override int GetHashCode() this.GlyphType, this.GlyphColor, this.FontStyle, - this.Dpi); + this.Dpi, + this.TextRun); } } diff --git a/src/SixLabors.Fonts/StreamFontMetrics.cs b/src/SixLabors.Fonts/StreamFontMetrics.cs index 5262efbfb..f210b2681 100644 --- a/src/SixLabors.Fonts/StreamFontMetrics.cs +++ b/src/SixLabors.Fonts/StreamFontMetrics.cs @@ -167,7 +167,7 @@ internal StreamFontMetrics( this.StrikeoutPosition = os2.StrikeoutPosition; this.UnderlinePosition = postTable.UnderlinePosition; - this.UnderlinePosition = postTable.UnderlinePosition; + this.UnderlineThickness = postTable.UnderlineThickness; this.ItalicAngle = postTable.ItalicAngle; this.kerningTable = kern; diff --git a/src/SixLabors.Fonts/TextRenderer.cs b/src/SixLabors.Fonts/TextRenderer.cs index 66328211d..229b30178 100644 --- a/src/SixLabors.Fonts/TextRenderer.cs +++ b/src/SixLabors.Fonts/TextRenderer.cs @@ -59,10 +59,10 @@ public void RenderText(ReadOnlySpan text, TextOptions options) foreach (GlyphLayout g in glyphsToRender) { - if (g.IsWhiteSpace()) - { - continue; - } + //if (g.IsWhiteSpace()) + //{ + // continue; + //} g.Glyph.RenderTo(this.renderer, g.Location, options); } From c34b1c2e2b3b3c035f27e1c3ac1879e60f575714 Mon Sep 17 00:00:00 2001 From: Scott Williams Date: Fri, 4 Mar 2022 19:09:46 +0000 Subject: [PATCH 20/48] add attributes --- samples/DrawWithImageSharp/Program.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/samples/DrawWithImageSharp/Program.cs b/samples/DrawWithImageSharp/Program.cs index 0fbfeecea..2c7e7f94f 100644 --- a/samples/DrawWithImageSharp/Program.cs +++ b/samples/DrawWithImageSharp/Program.cs @@ -42,11 +42,12 @@ public static void Main(string[] args) FontFamily tahoma = SystemFonts.Get("Tahoma"); var textRuns = new List { - new TextRun { Start = 4, End = 9, Font = uiFont.CreateFont(32, FontStyle.Bold) }, - new TextRun { Start = 26, End = 30, Font = uiFont.CreateFont(32, FontStyle.Italic) } + new TextRun { Start = 4, End = 9, Font = uiFont.CreateFont(32, FontStyle.Bold), TextAttributes = TextAttribute.Strikethrough }, + new TextRun { Start = 26, End = 30, Font = uiFont.CreateFont(32, FontStyle.Italic), TextAttributes = TextAttribute.Underline } }; RenderText(uiFont, "The quick brown fox jumps over the lazy dog", 32, textRuns: textRuns); + return; RenderText(font2, "\uFB01", pointSize: 11.25F); RenderText(fontWoff2, "\uFB01", pointSize: 11.25F); From 74512e3744558a192e20892cb0b8ea6b8beb859f Mon Sep 17 00:00:00 2001 From: Scott Williams Date: Fri, 4 Mar 2022 19:14:10 +0000 Subject: [PATCH 21/48] use better left hand side --- src/SixLabors.Fonts/GlyphMetrics.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/SixLabors.Fonts/GlyphMetrics.cs b/src/SixLabors.Fonts/GlyphMetrics.cs index 6a6d32a3d..861dc025b 100644 --- a/src/SixLabors.Fonts/GlyphMetrics.cs +++ b/src/SixLabors.Fonts/GlyphMetrics.cs @@ -341,10 +341,10 @@ void DrawLine(float thickness, float position) Vector2 scale = new Vector2(scaledPoint) / this.ScaleFactor * MirrorScale; - var tl = (new Vector2(0, top) * scale) + location; - var tr = (new Vector2(this.AdvanceWidth, top) * scale) + location; - var br = (new Vector2(this.AdvanceWidth, bottom) * scale) + location; - var bl = (new Vector2(0, bottom) * scale) + location; + var tl = (new Vector2(-this.LeftSideBearing, top) * scale) + location; + var tr = (new Vector2(this.AdvanceWidth + this.LeftSideBearing, top) * scale) + location; + var br = (new Vector2(this.AdvanceWidth + this.LeftSideBearing, bottom) * scale) + location; + var bl = (new Vector2(-this.LeftSideBearing, bottom) * scale) + location; tl.Y = MathF.Ceiling(tl.Y); tr.Y = MathF.Ceiling(tr.Y); From a1441e91ddbd7caf0fb0da04caa3080a21e786a0 Mon Sep 17 00:00:00 2001 From: Scott Williams Date: Fri, 4 Mar 2022 19:21:44 +0000 Subject: [PATCH 22/48] drop commented code --- src/SixLabors.Fonts/TextRenderer.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/SixLabors.Fonts/TextRenderer.cs b/src/SixLabors.Fonts/TextRenderer.cs index 229b30178..40b3fade4 100644 --- a/src/SixLabors.Fonts/TextRenderer.cs +++ b/src/SixLabors.Fonts/TextRenderer.cs @@ -59,11 +59,6 @@ public void RenderText(ReadOnlySpan text, TextOptions options) foreach (GlyphLayout g in glyphsToRender) { - //if (g.IsWhiteSpace()) - //{ - // continue; - //} - g.Glyph.RenderTo(this.renderer, g.Location, options); } From f256682ebf5415cd1c12aafa00dd684c4522f110 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sat, 5 Mar 2022 14:41:13 +1100 Subject: [PATCH 23/48] Fix scaled line offset, lockdown glyph rendering, fix bidi tests --- samples/DrawWithImageSharp/Program.cs | 4 +- src/SixLabors.Fonts/Font.cs | 13 ++++++- src/SixLabors.Fonts/GlyphMetrics.cs | 39 ++++++++++--------- .../GlyphRendererParameters.cs | 27 ++++++++----- src/SixLabors.Fonts/IColorGlyphRenderer.cs | 2 +- .../SixLabors.Fonts.Tests/FontLoaderTests.cs | 31 ++++++++------- .../SixLabors.Fonts.Tests/FontMetricsTests.cs | 12 ------ tests/SixLabors.Fonts.Tests/GlyphTests.cs | 25 ++++++------ .../Unicode/BidiAlgorithmTests.cs | 36 ++++++++--------- 9 files changed, 100 insertions(+), 89 deletions(-) diff --git a/samples/DrawWithImageSharp/Program.cs b/samples/DrawWithImageSharp/Program.cs index 2c7e7f94f..f6e524a62 100644 --- a/samples/DrawWithImageSharp/Program.cs +++ b/samples/DrawWithImageSharp/Program.cs @@ -42,8 +42,8 @@ public static void Main(string[] args) FontFamily tahoma = SystemFonts.Get("Tahoma"); var textRuns = new List { - new TextRun { Start = 4, End = 9, Font = uiFont.CreateFont(32, FontStyle.Bold), TextAttributes = TextAttribute.Strikethrough }, - new TextRun { Start = 26, End = 30, Font = uiFont.CreateFont(32, FontStyle.Italic), TextAttributes = TextAttribute.Underline } + new TextRun { Start = 4, End = 9, Font = uiFont.CreateFont(32, FontStyle.Bold), TextAttributes = TextAttribute.Strikethrough | TextAttribute.Superscript }, + new TextRun { Start = 26, End = 30, Font = uiFont.CreateFont(32, FontStyle.Italic), TextAttributes = TextAttribute.Underline | TextAttribute.Subscript } }; RenderText(uiFont, "The quick brown fox jumps over the lazy dog", 32, textRuns: textRuns); diff --git a/src/SixLabors.Fonts/Font.cs b/src/SixLabors.Fonts/Font.cs index 0c0387fe0..5a9fa1f3f 100644 --- a/src/SixLabors.Fonts/Font.cs +++ b/src/SixLabors.Fonts/Font.cs @@ -149,10 +149,21 @@ public bool TryGetPath([NotNullWhen(true)] out string? path) /// Options for enabling color font support during layout and rendering. /// Returns the glyph public IEnumerable GetGlyphs(CodePoint codePoint, ColorFontSupport support) + => this.GetGlyphs(codePoint, TextAttribute.None, support); + + /// + /// Gets the glyphs for the given codepoint. + /// + /// The code point of the character. + /// The text attributes to apply to the glyphs. + /// Options for enabling color font support during layout and rendering. + /// Returns the glyph + public IEnumerable GetGlyphs(CodePoint codePoint, TextAttribute textAttributes, ColorFontSupport support) { + TextRun textRun = new() { Start = 0, End = 1, Font = this, TextAttributes = textAttributes }; foreach (GlyphMetrics metrics in this.FontMetrics.GetGlyphMetrics(codePoint, support)) { - yield return new(metrics, this.Size); + yield return new(GlyphMetrics.CloneForRendering(metrics, textRun, codePoint), this.Size); } } diff --git a/src/SixLabors.Fonts/GlyphMetrics.cs b/src/SixLabors.Fonts/GlyphMetrics.cs index 861dc025b..81141f16a 100644 --- a/src/SixLabors.Fonts/GlyphMetrics.cs +++ b/src/SixLabors.Fonts/GlyphMetrics.cs @@ -18,7 +18,7 @@ public class GlyphMetrics private GlyphVector vector; private readonly Dictionary scaledVector = new(); private Vector2 offset = Vector2.Zero; - private TextRun textRun = new(); + private TextRun? textRun; internal GlyphMetrics( StreamFontMetrics font, @@ -222,16 +222,16 @@ internal FontRectangle GetBoundingBox(Vector2 origin, float scaledPointSize) /// The location. /// The options used to influence the rendering of this glyph. /// Too many control points - public void RenderTo(IGlyphRenderer surface, float pointSize, Vector2 location, TextOptions options) + internal void RenderTo(IGlyphRenderer surface, float pointSize, Vector2 location, TextOptions options) { float dpi = options.Dpi; location *= dpi; - float scaledPoint = dpi * pointSize; - FontRectangle box = this.GetBoundingBox(location, scaledPoint); - var parameters = new GlyphRendererParameters(this, this.textRun, pointSize, dpi); + // TextRun is never null here as rendering is only accessable via a Glyph which + // uses the cloned metrics instance. + var parameters = new GlyphRendererParameters(this, this.textRun!, pointSize, dpi); if (surface.BeginGlyph(box, parameters)) { @@ -286,7 +286,7 @@ public void RenderTo(IGlyphRenderer surface, float pointSize, Vector2 location, else { // If both first and last points are off-curve, start at their middle. - Vector2 startPoint = (curr + next) / 2; + Vector2 startPoint = (curr + next) * .5F; surface.MoveTo(startPoint); } } @@ -313,13 +313,13 @@ public void RenderTo(IGlyphRenderer surface, float pointSize, Vector2 location, if (!onCurves[prevIndex]) { - prev2 = (curr + prev) / 2; + prev2 = (curr + prev) * .5F; surface.LineTo(prev2); } if (!onCurves[nextIndex]) { - next2 = (curr + next) / 2; + next2 = (curr + next) * .5F; } surface.LineTo(prev2); @@ -335,16 +335,17 @@ void DrawLine(float thickness, float position) { surface.BeginFigure(); - var height = thickness; - var top = position - (height / 2); - var bottom = top + height; + float height = thickness; + float top = position - (height * .5F); + float bottom = top + height; Vector2 scale = new Vector2(scaledPoint) / this.ScaleFactor * MirrorScale; + Vector2 offset = location + (this.offset * scale * MirrorScale); - var tl = (new Vector2(-this.LeftSideBearing, top) * scale) + location; - var tr = (new Vector2(this.AdvanceWidth + this.LeftSideBearing, top) * scale) + location; - var br = (new Vector2(this.AdvanceWidth + this.LeftSideBearing, bottom) * scale) + location; - var bl = (new Vector2(-this.LeftSideBearing, bottom) * scale) + location; + Vector2 tl = (new Vector2(-this.LeftSideBearing, top) * scale) + offset; + Vector2 tr = (new Vector2(this.AdvanceWidth + this.LeftSideBearing, top) * scale) + offset; + Vector2 br = (new Vector2(this.AdvanceWidth + this.LeftSideBearing, bottom) * scale) + offset; + Vector2 bl = (new Vector2(-this.LeftSideBearing, bottom) * scale) + offset; tl.Y = MathF.Ceiling(tl.Y); tr.Y = MathF.Ceiling(tr.Y); @@ -359,13 +360,15 @@ void DrawLine(float thickness, float position) surface.EndFigure(); } - // lets figure out underline - if ((this.textRun.TextAttributes & TextAttribute.Underline) == TextAttribute.Underline) + // Add underline and stroke. + // TextRun is never null here as rendering is only accessable via a Glyph which + // uses the cloned metrics instance. + if ((this.textRun!.TextAttributes & TextAttribute.Underline) == TextAttribute.Underline) { DrawLine(this.FontMetrics.UnderlineThickness, this.FontMetrics.UnderlinePosition); } - if ((this.textRun.TextAttributes & TextAttribute.Strikethrough) == TextAttribute.Strikethrough) + if ((this.textRun!.TextAttributes & TextAttribute.Strikethrough) == TextAttribute.Strikethrough) { DrawLine(this.FontMetrics.StrikeoutSize, this.FontMetrics.StrikeoutPosition); } diff --git a/src/SixLabors.Fonts/GlyphRendererParameters.cs b/src/SixLabors.Fonts/GlyphRendererParameters.cs index 6f54ac74d..2413b524a 100644 --- a/src/SixLabors.Fonts/GlyphRendererParameters.cs +++ b/src/SixLabors.Fonts/GlyphRendererParameters.cs @@ -3,6 +3,7 @@ using System; using System.Diagnostics; +using SixLabors.Fonts.Unicode; namespace SixLabors.Fonts { @@ -13,16 +14,17 @@ namespace SixLabors.Fonts [DebuggerDisplay("GlyphIndex = {GlyphIndex}, PointSize = {PointSize}, DpiX = {DpiX}, DpiY = {DpiY}")] public readonly struct GlyphRendererParameters : IEquatable { - internal GlyphRendererParameters(GlyphMetrics glyph, TextRun textRun, float pointSize, float dpi) + internal GlyphRendererParameters(GlyphMetrics metrics, TextRun textRun, float pointSize, float dpi) { - this.Font = glyph.FontMetrics.Description.FontNameInvariantCulture?.ToUpper() ?? string.Empty; - this.FontStyle = glyph.FontMetrics.Description.Style; - this.GlyphIndex = glyph.GlyphId; + this.Font = metrics.FontMetrics.Description.FontNameInvariantCulture?.ToUpper() ?? string.Empty; + this.FontStyle = metrics.FontMetrics.Description.Style; + this.GlyphIndex = metrics.GlyphId; this.PointSize = pointSize; this.Dpi = dpi; - this.GlyphType = glyph.GlyphType; - this.GlyphColor = glyph.GlyphColor ?? default; + this.GlyphType = metrics.GlyphType; + this.GlyphColor = metrics.GlyphColor ?? default; this.TextRun = textRun; + this.CodePoint = metrics.CodePoint; } /// @@ -31,7 +33,7 @@ internal GlyphRendererParameters(GlyphMetrics glyph, TextRun textRun, float poin public string Font { get; } /// - /// Gets the type of this glyph. + /// Gets the color details of this glyph. /// public GlyphColor GlyphColor { get; } @@ -41,22 +43,27 @@ internal GlyphRendererParameters(GlyphMetrics glyph, TextRun textRun, float poin public GlyphType GlyphType { get; } /// - /// Gets the style of the Font this glyph belongs to. + /// Gets the style of the font this glyph belongs to. /// public FontStyle FontStyle { get; } /// - /// Gets the index of the glyph. + /// Gets the index of the glyph within the font tables. /// public ushort GlyphIndex { get; } + /// + /// Gets the codepoint represented by this glyph. + /// + public CodePoint CodePoint { get; } + /// /// Gets the rendered point size. /// public float PointSize { get; } /// - /// Gets the dots-per-inch we are rendering at. + /// Gets the dots-per-inch the glyph is to be rendered at. /// public float Dpi { get; } diff --git a/src/SixLabors.Fonts/IColorGlyphRenderer.cs b/src/SixLabors.Fonts/IColorGlyphRenderer.cs index 996b15ff0..5b76f4bc5 100644 --- a/src/SixLabors.Fonts/IColorGlyphRenderer.cs +++ b/src/SixLabors.Fonts/IColorGlyphRenderer.cs @@ -18,7 +18,7 @@ public interface IColorGlyphRenderer : IGlyphRenderer } /// - /// Provides access to the the color details for the current glyph. + /// Provides access to the color details for the current glyph. /// public readonly struct GlyphColor { diff --git a/tests/SixLabors.Fonts.Tests/FontLoaderTests.cs b/tests/SixLabors.Fonts.Tests/FontLoaderTests.cs index 6f126bb0f..e012182e4 100644 --- a/tests/SixLabors.Fonts.Tests/FontLoaderTests.cs +++ b/tests/SixLabors.Fonts.Tests/FontLoaderTests.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System.Linq; +using System.Numerics; using SixLabors.Fonts.Unicode; using Xunit; @@ -39,9 +40,9 @@ public void LoadFont_WithTtfFormat() { Font font = new FontCollection().Add(TestFonts.OpenSansFile).CreateFont(12); - GlyphMetrics glyph = font.FontMetrics.GetGlyphMetrics(new CodePoint('A'), ColorFontSupport.None).First(); - var r = new GlyphRenderer(); - glyph.RenderTo(r, font.Size, System.Numerics.Vector2.Zero, new TextOptions(font)); + Glyph glyph = font.GetGlyphs(new CodePoint('A'), ColorFontSupport.None).First(); + GlyphRenderer r = new(); + glyph.RenderTo(r, Vector2.Zero, new TextOptions(font)); Assert.Equal(37, r.ControlPoints.Count); Assert.Single(r.GlyphKeys); @@ -53,9 +54,9 @@ public void LoadFont_WithWoff1Format() { Font font = new FontCollection().Add(TestFonts.OpenSansFileWoff1).CreateFont(12); - GlyphMetrics glyph = font.FontMetrics.GetGlyphMetrics(new CodePoint('A'), ColorFontSupport.None).First(); - var r = new GlyphRenderer(); - glyph.RenderTo(r, font.Size, System.Numerics.Vector2.Zero, new TextOptions(font)); + Glyph glyph = font.GetGlyphs(new CodePoint('A'), ColorFontSupport.None).First(); + GlyphRenderer r = new(); + glyph.RenderTo(r, Vector2.Zero, new TextOptions(font)); Assert.Equal(37, r.ControlPoints.Count); Assert.Single(r.GlyphKeys); @@ -86,9 +87,9 @@ public void LoadFont_WithWoff2Format() { Font font = new FontCollection().Add(TestFonts.OpensSansWoff2Data()).CreateFont(12); - GlyphMetrics glyph = font.FontMetrics.GetGlyphMetrics(new CodePoint('A'), ColorFontSupport.None).First(); - var r = new GlyphRenderer(); - glyph.RenderTo(r, font.Size, System.Numerics.Vector2.Zero, new TextOptions(font)); + Glyph glyph = font.GetGlyphs(new CodePoint('A'), ColorFontSupport.None).First(); + GlyphRenderer r = new(); + glyph.RenderTo(r, Vector2.Zero, new TextOptions(font)); Assert.Equal(37, r.ControlPoints.Count); Assert.Single(r.GlyphKeys); @@ -104,9 +105,9 @@ public void LoadFont() Assert.Equal("SixLaborsSampleAB regular", font.FontMetrics.Description.FontNameInvariantCulture); Assert.Equal("Regular", font.FontMetrics.Description.FontSubFamilyNameInvariantCulture); - GlyphMetrics glyph = font.FontMetrics.GetGlyphMetrics(new CodePoint('a'), ColorFontSupport.None).First(); - var r = new GlyphRenderer(); - glyph.RenderTo(r, font.Size, System.Numerics.Vector2.Zero, new TextOptions(font)); + Glyph glyph = font.GetGlyphs(new CodePoint('a'), ColorFontSupport.None).First(); + GlyphRenderer r = new(); + glyph.RenderTo(r, Vector2.Zero, new TextOptions(font)); // the test font only has characters .notdef, 'a' & 'b' defined Assert.Equal(6, r.ControlPoints.Distinct().Count()); @@ -120,9 +121,9 @@ public void LoadFontWoff() Assert.Equal("SixLaborsSampleAB regular", font.FontMetrics.Description.FontNameInvariantCulture); Assert.Equal("Regular", font.FontMetrics.Description.FontSubFamilyNameInvariantCulture); - GlyphMetrics glyph = font.FontMetrics.GetGlyphMetrics(new CodePoint('a'), ColorFontSupport.None).First(); - var r = new GlyphRenderer(); - glyph.RenderTo(r, 12, System.Numerics.Vector2.Zero, new TextOptions(font)); + Glyph glyph = font.GetGlyphs(new CodePoint('a'), ColorFontSupport.None).First(); + GlyphRenderer r = new(); + glyph.RenderTo(r, Vector2.Zero, new TextOptions(font)); // the test font only has characters .notdef, 'a' & 'b' defined Assert.Equal(6, r.ControlPoints.Distinct().Count()); diff --git a/tests/SixLabors.Fonts.Tests/FontMetricsTests.cs b/tests/SixLabors.Fonts.Tests/FontMetricsTests.cs index f28cd3a2f..45635a93e 100644 --- a/tests/SixLabors.Fonts.Tests/FontMetricsTests.cs +++ b/tests/SixLabors.Fonts.Tests/FontMetricsTests.cs @@ -61,9 +61,6 @@ public void GlyphMetricsMatchesReference() var codePoint = new CodePoint('A'); GlyphMetrics glyphMetrics = font.FontMetrics.GetGlyphMetrics(codePoint, ColorFontSupport.None).First(); - GlyphMetrics glyphMetrics1 = font.GetGlyphs(codePoint, ColorFontSupport.None).First().GlyphMetrics; - - Assert.Equal(glyphMetrics, glyphMetrics1); Assert.Equal(codePoint, glyphMetrics.CodePoint); Assert.Equal(font.FontMetrics.UnitsPerEm, glyphMetrics.UnitsPerEm); @@ -88,9 +85,6 @@ public void GlyphMetricsMatchesReference_WithWoff1format() var codePoint = new CodePoint('A'); GlyphMetrics glyphMetrics = font.FontMetrics.GetGlyphMetrics(codePoint, ColorFontSupport.None).First(); - GlyphMetrics glyphMetrics1 = font.GetGlyphs(codePoint, ColorFontSupport.None).First().GlyphMetrics; - - Assert.Equal(glyphMetrics, glyphMetrics1); Assert.Equal(codePoint, glyphMetrics.CodePoint); Assert.Equal(font.FontMetrics.UnitsPerEm, glyphMetrics.UnitsPerEm); @@ -116,9 +110,6 @@ public void GlyphMetricsMatchesReference_WithWoff2format() var codePoint = new CodePoint('A'); GlyphMetrics glyphMetrics = font.FontMetrics.GetGlyphMetrics(codePoint, ColorFontSupport.None).First(); - GlyphMetrics glyphMetrics1 = font.GetGlyphs(codePoint, ColorFontSupport.None).First().GlyphMetrics; - - Assert.Equal(glyphMetrics, glyphMetrics1); Assert.Equal(codePoint, glyphMetrics.CodePoint); Assert.Equal(font.FontMetrics.UnitsPerEm, glyphMetrics.UnitsPerEm); @@ -144,9 +135,6 @@ public void GlyphMetricsVerticalMatchesReference() var codePoint = new CodePoint('A'); GlyphMetrics glyphMetrics = font.FontMetrics.GetGlyphMetrics(codePoint, ColorFontSupport.None).First(); - GlyphMetrics glyphMetrics1 = font.GetGlyphs(codePoint, ColorFontSupport.None).First().GlyphMetrics; - - Assert.Equal(glyphMetrics, glyphMetrics1); // Position 0. Assert.Equal(codePoint, glyphMetrics.CodePoint); diff --git a/tests/SixLabors.Fonts.Tests/GlyphTests.cs b/tests/SixLabors.Fonts.Tests/GlyphTests.cs index 1b842e20c..b54b58e01 100644 --- a/tests/SixLabors.Fonts.Tests/GlyphTests.cs +++ b/tests/SixLabors.Fonts.Tests/GlyphTests.cs @@ -25,18 +25,19 @@ public void RenderToPointAndSingleDPI() CodePoint codePoint = this.AsCodePoint(text); Font font = CreateFont(text); FontMetrics metrics = font.FontMetrics; - var glyph = new Glyph( - new GlyphMetrics( - (StreamFontMetrics)metrics, - codePoint, - new GlyphVector(new Vector2[0], new bool[0], new ushort[0], new Bounds(0, metrics.UnitsPerEm, 0, metrics.UnitsPerEm), Array.Empty()), - 0, - 0, - 0, - 0, - metrics.UnitsPerEm, - 0), - 10); + GlyphMetrics glyphMetrics = new( + (StreamFontMetrics)metrics, + codePoint, + new GlyphVector(new Vector2[0], new bool[0], new ushort[0], new Bounds(0, metrics.UnitsPerEm, 0, metrics.UnitsPerEm), Array.Empty()), + 0, + 0, + 0, + 0, + metrics.UnitsPerEm, + 0); + + TextRun textRun = new() { Start = 0, End = 1, Font = font }; + Glyph glyph = new(GlyphMetrics.CloneForRendering(glyphMetrics, textRun, codePoint), 10); Vector2 locationInFontSpace = new Vector2(99, 99) / 72; // glyph ends up 10px over due to offset in fake glyph glyph.RenderTo(this.renderer, locationInFontSpace, new TextOptions(font)); diff --git a/tests/SixLabors.Fonts.Tests/Unicode/BidiAlgorithmTests.cs b/tests/SixLabors.Fonts.Tests/Unicode/BidiAlgorithmTests.cs index 9f83efd38..7ca47f3d1 100644 --- a/tests/SixLabors.Fonts.Tests/Unicode/BidiAlgorithmTests.cs +++ b/tests/SixLabors.Fonts.Tests/Unicode/BidiAlgorithmTests.cs @@ -22,12 +22,12 @@ public void RendersKurdishTextCorrect() { // arrange Font arabicFont = new FontCollection().Add(TestFonts.ArabicFontFile).CreateFont(8); - var renderer = new ColorGlyphRenderer(); - string testStr = "نەما خانی هەتا چیڕۆکی عیشقی ئێمە داڕێژێ"; + ColorGlyphRenderer renderer = new(); + const string testStr = "نەما خانی هەتا چیڕۆکی عیشقی ئێمە داڕێژێ"; int[] expectedGlyphIndices = { - 0, 214, 0, 0, 139, 197, 0, 289, 0, 333, 336, 266, 221, 337, 242, 336, 276, 0, 0, 337, 188, 140, 170, - 0, 301, 336, 294, 140, 196, 140, 290, 0, 294 + 0, 214, 0, 0, 139, 197, 3, 0, 289, 0, 333, 3, 336, 266, 221, 337, 242, 3, 336, 276, 0, 0, 337, + 188, 3, 140, 170, 0, 301, 3, 336, 294, 140, 196, 3, 140, 290, 0, 294 }; // act @@ -47,11 +47,11 @@ public void RendersFarsiTextCorrect() // arrange Font arabicFont = new FontCollection().Add(TestFonts.ArabicFontFile).CreateFont(8); var renderer = new ColorGlyphRenderer(); - string testStr = "زناب فارسی را تک کمی سخت است"; + const string testStr = "زناب فارسی را تک کمی سخت است"; int[] expectedGlyphIndices = { - 168, 218, 139, 168, 195, 218, 336, 289, 276, 274, 170, 139, 203, 336, 218, 203, 140, 250, 157, 140, - 294, 207 + 168, 218, 139, 3, 168, 195, 218, 3, 336, 289, 276, 3, 274, 170, 3, 139, + 203, 3, 336, 218, 203, 140, 250, 3, 157, 140, 294, 207 }; // act @@ -70,13 +70,13 @@ public void RendersArabicTextWithPunctuationCorrectly() { // arrange Font arabicFont = new FontCollection().Add(TestFonts.TimesNewRomanFile).CreateFont(12); - var renderer = new ColorGlyphRenderer(); - string testStr = "زۆرمان باس کرد؛ باسی ئاو، کەش، هەوای کوردستان."; + ColorGlyphRenderer renderer = new(); + const string testStr = "زۆرمان باس کرد؛ باسی ئاو، کەش، هەوای کوردستان."; int[] expectedGlyphIndices = { - 17, 997, 910, 920, 947, 937, 941, 1006, 815, 821, 909, 1005, 1002, 1003, 748, - 949, 1002, 815, 748, 1005, 910, 907, 822, 947, 910, 913, 749, 937, 942, 815, - 945, 910, 913, 997, 910, 995, 941, 1574, 943 + 17, 997, 910, 920, 947, 937, 941, 1006, 815, 3, 821, 909, 1005, 1002, 1003, 3, 748, + 949, 1002, 815, 3, 748, 1005, 910, 907, 3, 822, 947, 910, 913, 3, 749, 937, 942, 815, + 3, 945, 910, 913, 3, 997, 910, 995, 941, 1574, 943 }; // act @@ -115,9 +115,9 @@ public void MixingArabicWordsWithNumbers_Works() { // arrange Font arabicFont = new FontCollection().Add(TestFonts.SegeouiFontFile).CreateFont(12); - var renderer = new ColorGlyphRenderer(); - string testStr = "لە ساڵی ١٢٣٤ ڕوویدا"; - int[] expectedGlyphIndices = { 2317, 3631, 2380, 2345, 2345, 2485, 2264, 2265, 2266, 2267, 2379, 2540, 2247, 2260, 2842, 2286 }; + ColorGlyphRenderer renderer = new(); + const string testStr = "لە ساڵی ١٢٣٤ ڕوویدا"; + 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)); @@ -135,9 +135,9 @@ public void MathematicalFormulasWithArabicText_Works() { // arrange Font arabicFont = new FontCollection().Add(TestFonts.SegeouiFontFile).CreateFont(12); - var renderer = new ColorGlyphRenderer(); - string testStr = "١١س + ٨ج = ٨٥١"; - int[] expectedGlyphIndices = { 2271, 2268, 2264, 32, 2322, 2271, 14, 2329, 2264, 2264 }; + ColorGlyphRenderer renderer = new(); + const string testStr = "١١س + ٨ج = ٨٥١"; + int[] expectedGlyphIndices = { 2271, 2268, 2264, 3, 32, 3, 2322, 2271, 3, 14, 3, 2329, 2264, 2264 }; // act TextRenderer.RenderTextTo(renderer, testStr, new TextOptions(arabicFont)); From f3de9c9a4ca12f70cdd55768524b7bda2c9e2a8d Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sat, 5 Mar 2022 16:28:14 +1100 Subject: [PATCH 24/48] Fix null check and optimize --- samples/DrawWithImageSharp/Program.cs | 16 +++++++++++++--- src/SixLabors.Fonts/GlyphMetrics.cs | 10 ++++++---- src/SixLabors.Fonts/TextLayout.cs | 7 ++++--- 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/samples/DrawWithImageSharp/Program.cs b/samples/DrawWithImageSharp/Program.cs index f6e524a62..53523b43b 100644 --- a/samples/DrawWithImageSharp/Program.cs +++ b/samples/DrawWithImageSharp/Program.cs @@ -8,6 +8,7 @@ using System.Numerics; using System.Text; using DrawWithImageSharp; +using SixLabors.Fonts.Unicode; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Drawing; using SixLabors.ImageSharp.Drawing.Processing; @@ -33,6 +34,7 @@ public static void Main(string[] args) FontFamily colorEmoji = fonts.Add(@"Fonts\Twemoji Mozilla.ttf"); FontFamily font2 = fonts.Add(@"Fonts\OpenSans-Regular.ttf"); FontFamily sunflower = fonts.Add(@"Fonts\Sunflower-Medium.ttf"); + FontFamily bugzilla = fonts.Add(@"Fonts\me_quran_volt_newmet.ttf"); #if OS_WINDOWS FontFamily emojiFont = SystemFonts.Get("Segoe UI Emoji"); @@ -46,7 +48,16 @@ public static void Main(string[] args) new TextRun { Start = 26, End = 30, Font = uiFont.CreateFont(32, FontStyle.Italic), TextAttributes = TextAttribute.Underline | TextAttribute.Subscript } }; - RenderText(uiFont, "The quick brown fox jumps over the lazy dog", 32, textRuns: textRuns); + RenderText(uiFont, "The quick brown fox jumps over the lazy dog", pointSize: 32, textRuns: textRuns); + + string arabic = "بِسْمِ ٱللَّهِ ٱلرَّحْمَٟنِ ٱلرَّحِيمِ"; + textRuns = new List + { + new TextRun { Start = 0, End = CodePoint.GetCodePointCount(arabic), TextAttributes = TextAttribute.Underline } + }; + + RenderText(bugzilla, arabic, pointSize: 72, textRuns: textRuns); + return; RenderText(font2, "\uFB01", pointSize: 11.25F); @@ -54,7 +65,6 @@ public static void Main(string[] args) RenderText(tahoma, "p", pointSize: 11.25F); RenderText(tahoma, "Lorem ipsum dolor sit amet", pointSize: 11.25F); RenderText(uiFont, "Soft\u00ADHyphen", pointSize: 72); - FontFamily bugzilla = fonts.Add(@"Fonts\me_quran_volt_newmet.ttf"); RenderText(uiFont, "Soft\u00ADHyphen", pointSize: 72); RenderText(bugzilla, "بِسْمِ ٱللَّهِ ٱلرَّحْمَٟنِ ٱلرَّحِيمِ", pointSize: 72); @@ -200,7 +210,7 @@ public static void RenderText( => RenderText( new TextOptions(new Font(font, pointSize)) { - WrappingLength = 400, + // WrappingLength = 400, FallbackFontFamilies = fallbackFonts?.ToArray(), TextRuns = textRuns?.ToArray() }, diff --git a/src/SixLabors.Fonts/GlyphMetrics.cs b/src/SixLabors.Fonts/GlyphMetrics.cs index 81141f16a..b67c08367 100644 --- a/src/SixLabors.Fonts/GlyphMetrics.cs +++ b/src/SixLabors.Fonts/GlyphMetrics.cs @@ -342,10 +342,12 @@ void DrawLine(float thickness, float position) Vector2 scale = new Vector2(scaledPoint) / this.ScaleFactor * MirrorScale; Vector2 offset = location + (this.offset * scale * MirrorScale); - Vector2 tl = (new Vector2(-this.LeftSideBearing, top) * scale) + offset; - Vector2 tr = (new Vector2(this.AdvanceWidth + this.LeftSideBearing, top) * scale) + offset; - Vector2 br = (new Vector2(this.AdvanceWidth + this.LeftSideBearing, bottom) * scale) + offset; - Vector2 bl = (new Vector2(-this.LeftSideBearing, bottom) * scale) + offset; + float lsb = this.LeftSideBearing; + float width = this.AdvanceWidth + lsb; + Vector2 tl = (new Vector2(-lsb, top) * scale) + offset; + Vector2 tr = (new Vector2(width, top) * scale) + offset; + Vector2 br = (new Vector2(width, bottom) * scale) + offset; + Vector2 bl = (new Vector2(-lsb, bottom) * scale) + offset; tl.Y = MathF.Ceiling(tl.Y); tr.Y = MathF.Ceiling(tr.Y); diff --git a/src/SixLabors.Fonts/TextLayout.cs b/src/SixLabors.Fonts/TextLayout.cs index 4dc1cfa77..0d9781032 100644 --- a/src/SixLabors.Fonts/TextLayout.cs +++ b/src/SixLabors.Fonts/TextLayout.cs @@ -29,7 +29,7 @@ public static IReadOnlyList GenerateLayout(ReadOnlySpan text, public static IReadOnlyList BuildTextRuns(ReadOnlySpan text, TextOptions options) { - if (options.TextRuns?.Count == 0) + if (options.TextRuns is null || options.TextRuns.Count == 0) { return new TextRun[] { @@ -657,8 +657,9 @@ private static TextBox BreakLines( else if (metrics.Length == 1 && (CodePoint.IsZeroWidthJoiner(codePoint) || CodePoint.IsZeroWidthNonJoiner(codePoint))) { // The zero-width joiner characters should be ignored when determining word or - // line break boundaries so are safe to skip here. Any existing instances are the result of font error. - // It multiple metrics are associated with code point, they are most likely the result of a substitution so we shouldn't ignore it. + // line break boundaries so are safe to skip here. Any existing instances are the result of font error + // unless multiple metrics are associated with code point. In this case they are most likely the result + // of a substitution and shouldn't be ignored. glyphAdvance = 0; } else if (!CodePoint.IsNewLine(codePoint)) From 09654f98ce035a805ff24705128cfb9c08164840 Mon Sep 17 00:00:00 2001 From: Scott Williams Date: Sat, 5 Mar 2022 13:12:54 +0000 Subject: [PATCH 25/48] better position underline/strikeout with better thinkness --- src/SixLabors.Fonts/GlyphMetrics.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/SixLabors.Fonts/GlyphMetrics.cs b/src/SixLabors.Fonts/GlyphMetrics.cs index 81141f16a..6c21c3c76 100644 --- a/src/SixLabors.Fonts/GlyphMetrics.cs +++ b/src/SixLabors.Fonts/GlyphMetrics.cs @@ -342,15 +342,19 @@ void DrawLine(float thickness, float position) Vector2 scale = new Vector2(scaledPoint) / this.ScaleFactor * MirrorScale; Vector2 offset = location + (this.offset * scale * MirrorScale); - Vector2 tl = (new Vector2(-this.LeftSideBearing, top) * scale) + offset; + Vector2 tl = (new Vector2(0, top) * scale) + offset; Vector2 tr = (new Vector2(this.AdvanceWidth + this.LeftSideBearing, top) * scale) + offset; Vector2 br = (new Vector2(this.AdvanceWidth + this.LeftSideBearing, bottom) * scale) + offset; - Vector2 bl = (new Vector2(-this.LeftSideBearing, bottom) * scale) + offset; + Vector2 bl = (new Vector2(0, bottom) * scale) + offset; tl.Y = MathF.Ceiling(tl.Y); tr.Y = MathF.Ceiling(tr.Y); - br.Y = MathF.Ceiling(br.Y); - bl.Y = MathF.Ceiling(bl.Y); + br.Y = MathF.Floor(br.Y); + bl.Y = MathF.Floor(bl.Y); + tl.X = MathF.Ceiling(tl.X); + bl.X = MathF.Ceiling(bl.X); + tr.X = MathF.Floor(tr.X); + br.X = MathF.Floor(br.X); surface.MoveTo(tl); surface.LineTo(bl); From 953fb9b560e96fd945f067cda45b22a0c50ea311 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sun, 6 Mar 2022 01:12:45 +1100 Subject: [PATCH 26/48] Fix line and layout rendering of Arabic scripts. --- samples/DrawWithImageSharp/Program.cs | 16 +++++--- src/SixLabors.Fonts/GlyphLayout.cs | 39 ++++++------------- src/SixLabors.Fonts/GlyphMetrics.cs | 31 ++++++++++++++- .../GlyphPositioningCollection.cs | 17 ++++++-- src/SixLabors.Fonts/GlyphShapingBounds.cs | 17 ++++---- src/SixLabors.Fonts/TextLayout.cs | 39 ++++++++++++++++--- src/SixLabors.Fonts/TextMeasurer.cs | 4 +- 7 files changed, 110 insertions(+), 53 deletions(-) diff --git a/samples/DrawWithImageSharp/Program.cs b/samples/DrawWithImageSharp/Program.cs index 53523b43b..3b21fdd85 100644 --- a/samples/DrawWithImageSharp/Program.cs +++ b/samples/DrawWithImageSharp/Program.cs @@ -44,11 +44,11 @@ public static void Main(string[] args) FontFamily tahoma = SystemFonts.Get("Tahoma"); var textRuns = new List { - new TextRun { Start = 4, End = 9, Font = uiFont.CreateFont(32, FontStyle.Bold), TextAttributes = TextAttribute.Strikethrough | TextAttribute.Superscript }, - new TextRun { Start = 26, End = 30, Font = uiFont.CreateFont(32, FontStyle.Italic), TextAttributes = TextAttribute.Underline | TextAttribute.Subscript } + new TextRun { Start = 4, End = 9, Font = uiFont.CreateFont(72, FontStyle.Bold), TextAttributes = TextAttribute.Strikethrough | TextAttribute.Superscript }, + new TextRun { Start = 26, End = 30, Font = uiFont.CreateFont(72, FontStyle.Italic), TextAttributes = TextAttribute.Underline | TextAttribute.Subscript } }; - RenderText(uiFont, "The quick brown fox jumps over the lazy dog", pointSize: 32, textRuns: textRuns); + RenderText(uiFont, "The quick brown fox jumps over the lazy dog", pointSize: 72, textRuns: textRuns); string arabic = "بِسْمِ ٱللَّهِ ٱلرَّحْمَٟنِ ٱلرَّحِيمِ"; textRuns = new List @@ -56,10 +56,14 @@ public static void Main(string[] args) new TextRun { Start = 0, End = CodePoint.GetCodePointCount(arabic), TextAttributes = TextAttribute.Underline } }; - RenderText(bugzilla, arabic, pointSize: 72, textRuns: textRuns); + RenderText(uiFont, arabic, pointSize: 72, textRuns: textRuns); + textRuns = new List + { + new TextRun { Start = 0, End = CodePoint.GetCodePointCount(arabic), TextAttributes = TextAttribute.Underline } + }; + RenderText(bugzilla, arabic, pointSize: 72, textRuns: textRuns); - return; RenderText(font2, "\uFB01", pointSize: 11.25F); RenderText(fontWoff2, "\uFB01", pointSize: 11.25F); RenderText(tahoma, "p", pointSize: 11.25F); @@ -67,7 +71,7 @@ public static void Main(string[] args) RenderText(uiFont, "Soft\u00ADHyphen", pointSize: 72); RenderText(uiFont, "Soft\u00ADHyphen", pointSize: 72); - RenderText(bugzilla, "بِسْمِ ٱللَّهِ ٱلرَّحْمَٟنِ ٱلرَّحِيمِ", pointSize: 72); + // RenderText(bugzilla, "بِسْمِ ٱللَّهِ ٱلرَّحْمَٟنِ ٱلرَّحِيمِ", pointSize: 72); RenderText(uiFont, "first\n\n\n\nl", pointSize: 20, fallbackFonts: new[] { font2 }); diff --git a/src/SixLabors.Fonts/GlyphLayout.cs b/src/SixLabors.Fonts/GlyphLayout.cs index 4c72d8626..fded486e8 100644 --- a/src/SixLabors.Fonts/GlyphLayout.cs +++ b/src/SixLabors.Fonts/GlyphLayout.cs @@ -22,6 +22,7 @@ internal GlyphLayout( { this.LineHeight = lineHeight; this.Glyph = glyph; + this.CodePoint = glyph.GlyphMetrics.CodePoint; this.Location = location; this.Width = width; this.Height = height; @@ -33,6 +34,11 @@ internal GlyphLayout( /// public Glyph Glyph { get; } + /// + /// Gets the codepoint represented by this glyph. + /// + public CodePoint CodePoint { get; } + /// /// Gets the location. /// @@ -62,7 +68,7 @@ internal GlyphLayout( /// Gets a value indicating whether the glyph represents a whitespace character. /// /// The . - public bool IsWhiteSpace() => CodePoint.IsWhiteSpace(this.Glyph.GlyphMetrics.CodePoint); + public bool IsWhiteSpace() => CodePoint.IsWhiteSpace(this.CodePoint); internal FontRectangle BoundingBox(float dpi) { @@ -79,33 +85,10 @@ internal FontRectangle BoundingBox(float dpi) /// public override string ToString() { - var sb = new StringBuilder(); - if (this.IsStartOfLine) - { - sb.Append('@'); - sb.Append(' '); - } - - if (this.IsWhiteSpace()) - { - sb.Append('!'); - } - - sb.Append('\''); - sb.Append(this.Glyph.GlyphMetrics.CodePoint.ToDebuggerDisplay()); - - sb.Append('\''); - sb.Append(' '); - - sb.Append(this.Location.X); - sb.Append(','); - sb.Append(this.Location.Y); - sb.Append(' '); - sb.Append(this.Width); - sb.Append('x'); - sb.Append(this.Height); - - return sb.ToString(); + string s = this.IsStartOfLine ? "@ " : string.Empty; + string ws = this.IsWhiteSpace() ? "!" : string.Empty; + Vector2 l = this.Location; + return $"{s}{ws}{this.CodePoint.ToDebuggerDisplay()} {l.X},{l.Y} {this.Width}x{this.Height}"; } } } diff --git a/src/SixLabors.Fonts/GlyphMetrics.cs b/src/SixLabors.Fonts/GlyphMetrics.cs index b67c08367..a1105c00d 100644 --- a/src/SixLabors.Fonts/GlyphMetrics.cs +++ b/src/SixLabors.Fonts/GlyphMetrics.cs @@ -47,7 +47,9 @@ internal GlyphMetrics( this.Height = bounds.Max.Y - bounds.Min.Y; this.GlyphType = glyphType; this.LeftSideBearing = leftSideBearing; + this.RightSideBearing = (short)(this.AdvanceWidth - this.LeftSideBearing - this.Width); this.TopSideBearing = topSideBearing; + this.BottomSideBearing = (short)(this.AdvanceHeight - this.TopSideBearing - this.Height); this.ScaleFactor = new(unitsPerEM * 72F); this.GlyphColor = glyphColor; } @@ -77,11 +79,21 @@ internal GlyphMetrics( /// public short LeftSideBearing { get; } + /// + /// Gets the right side bearing for horizontal layout, expressed in font units. + /// + public short RightSideBearing { get; } + /// /// Gets the top side bearing for vertical layout, expressed in font units. /// public short TopSideBearing { get; } + /// + /// Gets the bottom side bearing for vertical layout, expressed in font units. + /// + public short BottomSideBearing { get; } + /// /// Gets the width, expressed in font units. /// @@ -342,18 +354,35 @@ void DrawLine(float thickness, float position) Vector2 scale = new Vector2(scaledPoint) / this.ScaleFactor * MirrorScale; Vector2 offset = location + (this.offset * scale * MirrorScale); - float lsb = this.LeftSideBearing; + // Calculate the correct advance for the line. + // For zero advance glyphs we must calculate our own advance. + // We add a single font unit to account for rounding error. + float lsb = Math.Abs(this.LeftSideBearing) + 1; float width = this.AdvanceWidth + lsb; + if (this.AdvanceWidth == 0) + { + // Create advance width from bearing + width; + width = lsb + lsb + this.Width + Math.Abs(this.RightSideBearing) + 1; + } + Vector2 tl = (new Vector2(-lsb, top) * scale) + offset; Vector2 tr = (new Vector2(width, top) * scale) + offset; Vector2 br = (new Vector2(width, bottom) * scale) + offset; Vector2 bl = (new Vector2(-lsb, bottom) * scale) + offset; + // Clamp the horizontal components to a whole pixel. tl.Y = MathF.Ceiling(tl.Y); tr.Y = MathF.Ceiling(tr.Y); br.Y = MathF.Ceiling(br.Y); bl.Y = MathF.Ceiling(bl.Y); + // Do the same for vertical components. + // This helps with rounding error for certain fonts. + tl.X = MathF.Floor(tl.X); + tr.X = MathF.Ceiling(tr.X); + br.X = MathF.Ceiling(br.X); + bl.X = MathF.Floor(bl.X); + surface.MoveTo(tl); surface.LineTo(bl); surface.LineTo(br); diff --git a/src/SixLabors.Fonts/GlyphPositioningCollection.cs b/src/SixLabors.Fonts/GlyphPositioningCollection.cs index c3ab20b49..11a68bdce 100644 --- a/src/SixLabors.Fonts/GlyphPositioningCollection.cs +++ b/src/SixLabors.Fonts/GlyphPositioningCollection.cs @@ -237,7 +237,9 @@ public bool TryAdd(Font font, GlyphSubstitutionCollection collection) public void UpdatePosition(FontMetrics fontMetrics, ushort index) { GlyphShapingData data = this.GetGlyphShapingData(index); - if (!data.Bounds.IsDirty) + bool isDirtyXY = data.Bounds.IsDirtyXY; + bool isDirtyWH = data.Bounds.IsDirtyWH; + if (!isDirtyXY && !isDirtyWH) { return; } @@ -247,9 +249,16 @@ public void UpdatePosition(FontMetrics fontMetrics, ushort index) { if (m.GlyphId == glyphId && fontMetrics == m.FontMetrics) { - m.ApplyOffset((short)data.Bounds.X, (short)data.Bounds.Y); - m.SetAdvanceWidth((ushort)data.Bounds.Width); - m.SetAdvanceHeight((ushort)data.Bounds.Height); + if (isDirtyXY) + { + m.ApplyOffset((short)data.Bounds.X, (short)data.Bounds.Y); + } + + if (isDirtyWH) + { + m.SetAdvanceWidth((ushort)data.Bounds.Width); + m.SetAdvanceHeight((ushort)data.Bounds.Height); + } } } } diff --git a/src/SixLabors.Fonts/GlyphShapingBounds.cs b/src/SixLabors.Fonts/GlyphShapingBounds.cs index 5ee542b2e..89df2cfcd 100644 --- a/src/SixLabors.Fonts/GlyphShapingBounds.cs +++ b/src/SixLabors.Fonts/GlyphShapingBounds.cs @@ -24,7 +24,8 @@ public GlyphShapingBounds(int x, int y, int width, int height) this.y = y; this.width = width; this.height = height; - this.IsDirty = false; + this.IsDirtyXY = false; + this.IsDirtyWH = false; } public int X @@ -34,7 +35,7 @@ public int X set { this.x = value; - this.IsDirty = true; + this.IsDirtyXY = true; } } @@ -45,7 +46,7 @@ public int Y set { this.y = value; - this.IsDirty = true; + this.IsDirtyXY = true; } } @@ -56,7 +57,7 @@ public int Width set { this.width = value; - this.IsDirty = true; + this.IsDirtyWH = true; } } @@ -67,13 +68,15 @@ public int Height set { this.height = value; - this.IsDirty = true; + this.IsDirtyWH = true; } } - public bool IsDirty { get; private set; } + public bool IsDirtyXY { get; private set; } + + public bool IsDirtyWH { get; private set; } private string DebuggerDisplay - => FormattableString.Invariant($"{this.X} : {this.Y} : {this.Width} : {this.Height} : {this.IsDirty}"); + => FormattableString.Invariant($"{this.X} : {this.Y} : {this.Width} : {this.Height} : {this.IsDirtyXY} : {this.IsDirtyWH}"); } } diff --git a/src/SixLabors.Fonts/TextLayout.cs b/src/SixLabors.Fonts/TextLayout.cs index 0d9781032..d9404498e 100644 --- a/src/SixLabors.Fonts/TextLayout.cs +++ b/src/SixLabors.Fonts/TextLayout.cs @@ -324,11 +324,25 @@ private static IEnumerable LayoutLineHorizontal( foreach (GlyphMetrics metric in info.Metrics) { + // Advance Width & Height can be 0 which is fine for layout but not for measuring. + Vector2 scale = new Vector2(info.PointSize) / metric.ScaleFactor; + float advanceX = info.ScaledAdvance; + float advanceY = metric.AdvanceHeight * scale.Y; + if (advanceX == 0) + { + advanceX = (metric.LeftSideBearing + metric.Width + metric.RightSideBearing) * scale.X; + } + + if (advanceY == 0) + { + advanceY = (metric.TopSideBearing + metric.Height + metric.BottomSideBearing) * scale.Y; + } + glyphs.Add(new GlyphLayout( new Glyph(metric, info.PointSize), location, - info.ScaledAdvance, - metric.AdvanceHeight * (info.PointSize / metric.ScaleFactor.Y), + advanceX, + advanceY, textBox.ScaledMaxLineHeight * options.LineSpacing, i == 0)); } @@ -443,11 +457,26 @@ private static IEnumerable LayoutLineVertical( foreach (GlyphMetrics metric in info.Metrics) { + Vector2 scale = new Vector2(info.PointSize) / metric.ScaleFactor; + float advanceX = xLineAdvance; + float advanceY = info.ScaledAdvance; + + // Advance Width & Height can be 0 which is fine for layout but not for measuring. + if (advanceX == 0) + { + advanceX = (metric.LeftSideBearing + metric.Width + metric.RightSideBearing) * scale.X; + } + + if (advanceY == 0) + { + advanceY = (metric.TopSideBearing + metric.Height + metric.BottomSideBearing) * scale.Y; + } + glyphs.Add(new GlyphLayout( new Glyph(metric, info.PointSize), - location + new Vector2((xWidth - (metric.AdvanceWidth * (info.PointSize / metric.ScaleFactor.X))) * .5F, 0), - xLineAdvance, - info.ScaledAdvance, + location + new Vector2((xWidth - (metric.AdvanceWidth * scale.X)) * .5F, 0), + advanceX, + advanceY, textBox.ScaledMaxLineHeight, i == 0)); } diff --git a/src/SixLabors.Fonts/TextMeasurer.cs b/src/SixLabors.Fonts/TextMeasurer.cs index aa74a4a78..29fcc3930 100644 --- a/src/SixLabors.Fonts/TextMeasurer.cs +++ b/src/SixLabors.Fonts/TextMeasurer.cs @@ -97,9 +97,9 @@ internal static FontRectangle GetSize(IReadOnlyList glyphLayouts, f float top = glyphLayouts.Min(x => x.Location.Y); float left = glyphLayouts.Min(x => x.Location.X); - // Avoid trimming zero-width marks that extend past the bounds of their base. + // Avoid trimming zero-width/height marks that extend past the bounds of their base. float right = glyphLayouts.Max(x => Math.Max(x.Location.X + x.Width, x.BoundingBox(1F).Right)); - float bottom = glyphLayouts.Max(x => x.Location.Y + x.LineHeight); + float bottom = glyphLayouts.Max(x => Math.Max(x.Location.Y + x.LineHeight, x.BoundingBox(1F).Top + x.LineHeight)); Vector2 topLeft = new Vector2(left, top) * dpi; Vector2 bottomRight = new Vector2(right, bottom) * dpi; From 4b63c79932a4e1d171c0c577c9c392ff11a5fe37 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sun, 6 Mar 2022 01:22:28 +1100 Subject: [PATCH 27/48] Update Program.cs --- samples/DrawWithImageSharp/Program.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/samples/DrawWithImageSharp/Program.cs b/samples/DrawWithImageSharp/Program.cs index 3b21fdd85..ba9f191b6 100644 --- a/samples/DrawWithImageSharp/Program.cs +++ b/samples/DrawWithImageSharp/Program.cs @@ -71,7 +71,6 @@ public static void Main(string[] args) RenderText(uiFont, "Soft\u00ADHyphen", pointSize: 72); RenderText(uiFont, "Soft\u00ADHyphen", pointSize: 72); - // RenderText(bugzilla, "بِسْمِ ٱللَّهِ ٱلرَّحْمَٟنِ ٱلرَّحِيمِ", pointSize: 72); RenderText(uiFont, "first\n\n\n\nl", pointSize: 20, fallbackFonts: new[] { font2 }); From 513d03538a92043af228ebbd2b839ab84b1ab0a1 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sun, 6 Mar 2022 15:22:28 +1100 Subject: [PATCH 28/48] Fix overlong stroke --- src/SixLabors.Fonts/GlyphLayout.cs | 1 - src/SixLabors.Fonts/GlyphMetrics.cs | 7 +++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/SixLabors.Fonts/GlyphLayout.cs b/src/SixLabors.Fonts/GlyphLayout.cs index fded486e8..f82392aa8 100644 --- a/src/SixLabors.Fonts/GlyphLayout.cs +++ b/src/SixLabors.Fonts/GlyphLayout.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. using System.Numerics; -using System.Text; using SixLabors.Fonts.Unicode; namespace SixLabors.Fonts diff --git a/src/SixLabors.Fonts/GlyphMetrics.cs b/src/SixLabors.Fonts/GlyphMetrics.cs index a1105c00d..d3d30840f 100644 --- a/src/SixLabors.Fonts/GlyphMetrics.cs +++ b/src/SixLabors.Fonts/GlyphMetrics.cs @@ -362,7 +362,7 @@ void DrawLine(float thickness, float position) if (this.AdvanceWidth == 0) { // Create advance width from bearing + width; - width = lsb + lsb + this.Width + Math.Abs(this.RightSideBearing) + 1; + width = lsb + this.Width; } Vector2 tl = (new Vector2(-lsb, top) * scale) + offset; @@ -373,11 +373,10 @@ void DrawLine(float thickness, float position) // Clamp the horizontal components to a whole pixel. tl.Y = MathF.Ceiling(tl.Y); tr.Y = MathF.Ceiling(tr.Y); - br.Y = MathF.Ceiling(br.Y); - bl.Y = MathF.Ceiling(bl.Y); + br.Y = MathF.Floor(br.Y); + bl.Y = MathF.Floor(bl.Y); // Do the same for vertical components. - // This helps with rounding error for certain fonts. tl.X = MathF.Floor(tl.X); tr.X = MathF.Ceiling(tr.X); br.X = MathF.Ceiling(br.X); From a75eb024c37ae88197cf95188fcd89c1f6114204 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sun, 6 Mar 2022 21:30:36 +1100 Subject: [PATCH 29/48] Don't use rsb for fake advance measurements --- src/SixLabors.Fonts/TextLayout.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/SixLabors.Fonts/TextLayout.cs b/src/SixLabors.Fonts/TextLayout.cs index d9404498e..6c677d9f2 100644 --- a/src/SixLabors.Fonts/TextLayout.cs +++ b/src/SixLabors.Fonts/TextLayout.cs @@ -330,12 +330,12 @@ private static IEnumerable LayoutLineHorizontal( float advanceY = metric.AdvanceHeight * scale.Y; if (advanceX == 0) { - advanceX = (metric.LeftSideBearing + metric.Width + metric.RightSideBearing) * scale.X; + advanceX = (metric.LeftSideBearing + metric.Width) * scale.X; } if (advanceY == 0) { - advanceY = (metric.TopSideBearing + metric.Height + metric.BottomSideBearing) * scale.Y; + advanceY = (metric.TopSideBearing + metric.Height) * scale.Y; } glyphs.Add(new GlyphLayout( @@ -464,12 +464,12 @@ private static IEnumerable LayoutLineVertical( // Advance Width & Height can be 0 which is fine for layout but not for measuring. if (advanceX == 0) { - advanceX = (metric.LeftSideBearing + metric.Width + metric.RightSideBearing) * scale.X; + advanceX = (metric.LeftSideBearing + metric.Width) * scale.X; } if (advanceY == 0) { - advanceY = (metric.TopSideBearing + metric.Height + metric.BottomSideBearing) * scale.Y; + advanceY = (metric.TopSideBearing + metric.Height) * scale.Y; } glyphs.Add(new GlyphLayout( From b00241574f1a54aae7eea47c06b45e72f0556f78 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sun, 6 Mar 2022 21:54:08 +1100 Subject: [PATCH 30/48] Always Ceil string measuring results. --- src/SixLabors.Fonts/TextMeasurer.cs | 41 ++++++++++++++++--- .../Issues/Issues_180.cs | 4 +- .../SixLabors.Fonts.Tests/TextLayoutTests.cs | 12 +++--- 3 files changed, 43 insertions(+), 14 deletions(-) diff --git a/src/SixLabors.Fonts/TextMeasurer.cs b/src/SixLabors.Fonts/TextMeasurer.cs index 29fcc3930..5701f5c1b 100644 --- a/src/SixLabors.Fonts/TextMeasurer.cs +++ b/src/SixLabors.Fonts/TextMeasurer.cs @@ -94,18 +94,47 @@ internal static FontRectangle GetSize(IReadOnlyList glyphLayouts, f return FontRectangle.Empty; } - float top = glyphLayouts.Min(x => x.Location.Y); - float left = glyphLayouts.Min(x => x.Location.X); + float left = int.MaxValue; + float top = int.MaxValue; + float bottom = int.MinValue; + float right = int.MinValue; + for (int i = 0; i < glyphLayouts.Count; i++) + { + GlyphLayout glyph = glyphLayouts[i]; + Vector2 location = glyph.Location; + float lineHeight = glyph.LineHeight; + + // Avoid trimming zero-width/height marks that extend past the bounds of their base. + FontRectangle box = glyph.BoundingBox(1F); + float advanceX = Math.Max(location.X + glyph.Width, box.Right); + float advanceY = Math.Max(location.Y + lineHeight, box.Top + lineHeight); + + if (left > location.X) + { + left = location.X; + } - // Avoid trimming zero-width/height marks that extend past the bounds of their base. - float right = glyphLayouts.Max(x => Math.Max(x.Location.X + x.Width, x.BoundingBox(1F).Right)); - float bottom = glyphLayouts.Max(x => Math.Max(x.Location.Y + x.LineHeight, x.BoundingBox(1F).Top + x.LineHeight)); + if (top > location.Y) + { + top = location.Y; + } + + if (right < advanceX) + { + right = advanceX; + } + + if (bottom < advanceY) + { + bottom = advanceY; + } + } Vector2 topLeft = new Vector2(left, top) * dpi; Vector2 bottomRight = new Vector2(right, bottom) * dpi; Vector2 size = bottomRight - topLeft; - return new FontRectangle(0, 0, size.X, size.Y); + return new FontRectangle(0, 0, MathF.Ceiling(size.X), MathF.Ceiling(size.Y)); } internal static FontRectangle GetBounds(IReadOnlyList glyphLayouts, float dpi) diff --git a/tests/SixLabors.Fonts.Tests/Issues/Issues_180.cs b/tests/SixLabors.Fonts.Tests/Issues/Issues_180.cs index 2317368cb..33b9997ae 100644 --- a/tests/SixLabors.Fonts.Tests/Issues/Issues_180.cs +++ b/tests/SixLabors.Fonts.Tests/Issues/Issues_180.cs @@ -15,8 +15,8 @@ public void CorrectlySetsHeightMetrics() FontRectangle size = TextMeasurer.Measure("H", new TextOptions(font)); - Assert.Equal(17.6, size.Width, 1); - Assert.Equal(30.6, size.Height, 1); + Assert.Equal(18, size.Width, 1); + Assert.Equal(31, size.Height, 1); } } } diff --git a/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs b/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs index 1ceb6b544..97c9be22e 100644 --- a/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs +++ b/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs @@ -251,12 +251,12 @@ public void MeasureTextWordWrappingVerticalRightLeft(string text, float height, #if OS_WINDOWS [Theory] - [InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious Taumatawhakatangihangakoauauotamateaturipukakapikimaungahoronukupokaiwhenuakitanatahu グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalTopBottom, WordBreaking.Normal, 133.0078, 870.6345)] - [InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious Taumatawhakatangihangakoauauotamateaturipukakapikimaungahoronukupokaiwhenuakitanatahu グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalTopBottom, WordBreaking.BreakAll, 159.6094, 399.9999)] - [InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalTopBottom, WordBreaking.KeepAll, 79.8047, 699.9998)] - [InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious Taumatawhakatangihangakoauauotamateaturipukakapikimaungahoronukupokaiwhenuakitanatahu グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalBottomTop, WordBreaking.Normal, 133.0078, 870.6345)] - [InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious Taumatawhakatangihangakoauauotamateaturipukakapikimaungahoronukupokaiwhenuakitanatahu グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalBottomTop, WordBreaking.BreakAll, 159.6094, 399.9999)] - [InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalBottomTop, WordBreaking.KeepAll, 79.8047, 699.9998)] + [InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious Taumatawhakatangihangakoauauotamateaturipukakapikimaungahoronukupokaiwhenuakitanatahu グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalTopBottom, WordBreaking.Normal, 134, 871)] + [InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious Taumatawhakatangihangakoauauotamateaturipukakapikimaungahoronukupokaiwhenuakitanatahu グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalTopBottom, WordBreaking.BreakAll, 160, 400)] + [InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalTopBottom, WordBreaking.KeepAll, 80, 700)] + [InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious Taumatawhakatangihangakoauauotamateaturipukakapikimaungahoronukupokaiwhenuakitanatahu グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalBottomTop, WordBreaking.Normal, 134, 871)] + [InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious Taumatawhakatangihangakoauauotamateaturipukakapikimaungahoronukupokaiwhenuakitanatahu グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalBottomTop, WordBreaking.BreakAll, 160, 400)] + [InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalBottomTop, WordBreaking.KeepAll, 80, 700)] public void MeasureTextWordBreak(string text, LayoutMode layoutMode, WordBreaking wordBreaking, float height, float width) { // Testing using Windows only to ensure that actual glyphs are rendered From d30b285fd5fe2c15d69a41183534f88b4e333490 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sun, 6 Mar 2022 23:54:54 +1100 Subject: [PATCH 31/48] Pixel perfect stroke. (Tested with drawing PR) --- src/SixLabors.Fonts/GlyphMetrics.cs | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/SixLabors.Fonts/GlyphMetrics.cs b/src/SixLabors.Fonts/GlyphMetrics.cs index d3d30840f..21005f70a 100644 --- a/src/SixLabors.Fonts/GlyphMetrics.cs +++ b/src/SixLabors.Fonts/GlyphMetrics.cs @@ -355,20 +355,17 @@ void DrawLine(float thickness, float position) Vector2 offset = location + (this.offset * scale * MirrorScale); // Calculate the correct advance for the line. - // For zero advance glyphs we must calculate our own advance. - // We add a single font unit to account for rounding error. - float lsb = Math.Abs(this.LeftSideBearing) + 1; - float width = this.AdvanceWidth + lsb; + float width = this.AdvanceWidth; if (this.AdvanceWidth == 0) { - // Create advance width from bearing + width; - width = lsb + this.Width; + // For zero advance glyphs we must calculate our advance width from bearing + width; + width = this.LeftSideBearing + this.Width; } - Vector2 tl = (new Vector2(-lsb, top) * scale) + offset; + Vector2 tl = (new Vector2(0, top) * scale) + offset; Vector2 tr = (new Vector2(width, top) * scale) + offset; Vector2 br = (new Vector2(width, bottom) * scale) + offset; - Vector2 bl = (new Vector2(-lsb, bottom) * scale) + offset; + Vector2 bl = (new Vector2(0, bottom) * scale) + offset; // Clamp the horizontal components to a whole pixel. tl.Y = MathF.Ceiling(tl.Y); @@ -378,8 +375,8 @@ void DrawLine(float thickness, float position) // Do the same for vertical components. tl.X = MathF.Floor(tl.X); - tr.X = MathF.Ceiling(tr.X); - br.X = MathF.Ceiling(br.X); + tr.X = MathF.Floor(tr.X); + br.X = MathF.Floor(br.X); bl.X = MathF.Floor(bl.X); surface.MoveTo(tl); From 205cb94742d6979a7f92379fcc9daa4dbc62cc53 Mon Sep 17 00:00:00 2001 From: Scott Williams Date: Sun, 6 Mar 2022 21:12:33 +0000 Subject: [PATCH 32/48] Split TextDecoration and TextAttribute and add decoration renderer --- samples/DrawWithImageSharp/Program.cs | 8 +- src/SixLabors.Fonts/GlyphMetrics.cs | 80 ++++++++++++++----- .../IGlyphDecorationRenderer.cs | 22 +++++ src/SixLabors.Fonts/TextAttribute.cs | 14 +--- src/SixLabors.Fonts/TextDecoration.cs | 34 ++++++++ src/SixLabors.Fonts/TextRun.cs | 5 ++ 6 files changed, 129 insertions(+), 34 deletions(-) create mode 100644 src/SixLabors.Fonts/IGlyphDecorationRenderer.cs create mode 100644 src/SixLabors.Fonts/TextDecoration.cs diff --git a/samples/DrawWithImageSharp/Program.cs b/samples/DrawWithImageSharp/Program.cs index ba9f191b6..cb16d156d 100644 --- a/samples/DrawWithImageSharp/Program.cs +++ b/samples/DrawWithImageSharp/Program.cs @@ -44,8 +44,8 @@ public static void Main(string[] args) FontFamily tahoma = SystemFonts.Get("Tahoma"); var textRuns = new List { - new TextRun { Start = 4, End = 9, Font = uiFont.CreateFont(72, FontStyle.Bold), TextAttributes = TextAttribute.Strikethrough | TextAttribute.Superscript }, - new TextRun { Start = 26, End = 30, Font = uiFont.CreateFont(72, FontStyle.Italic), TextAttributes = TextAttribute.Underline | TextAttribute.Subscript } + new TextRun { Start = 4, End = 9, Font = uiFont.CreateFont(72, FontStyle.Bold), TextAttributes = TextAttribute.Superscript, TextDecorations = TextDecoration.Strikeout }, + new TextRun { Start = 26, End = 30, Font = uiFont.CreateFont(72, FontStyle.Italic), TextAttributes = TextAttribute.Subscript, TextDecorations = TextDecoration.Underline } }; RenderText(uiFont, "The quick brown fox jumps over the lazy dog", pointSize: 72, textRuns: textRuns); @@ -53,14 +53,14 @@ public static void Main(string[] args) string arabic = "بِسْمِ ٱللَّهِ ٱلرَّحْمَٟنِ ٱلرَّحِيمِ"; textRuns = new List { - new TextRun { Start = 0, End = CodePoint.GetCodePointCount(arabic), TextAttributes = TextAttribute.Underline } + new TextRun { Start = 0, End = CodePoint.GetCodePointCount(arabic), TextDecorations = TextDecoration.Underline } }; RenderText(uiFont, arabic, pointSize: 72, textRuns: textRuns); textRuns = new List { - new TextRun { Start = 0, End = CodePoint.GetCodePointCount(arabic), TextAttributes = TextAttribute.Underline } + new TextRun { Start = 0, End = CodePoint.GetCodePointCount(arabic), TextDecorations = TextDecoration.Underline } }; RenderText(bugzilla, arabic, pointSize: 72, textRuns: textRuns); diff --git a/src/SixLabors.Fonts/GlyphMetrics.cs b/src/SixLabors.Fonts/GlyphMetrics.cs index 21005f70a..afc912938 100644 --- a/src/SixLabors.Fonts/GlyphMetrics.cs +++ b/src/SixLabors.Fonts/GlyphMetrics.cs @@ -343,14 +343,8 @@ internal void RenderTo(IGlyphRenderer surface, float pointSize, Vector2 location } } - void DrawLine(float thickness, float position) + (Vector2 start, Vector2 end, float thickness) GetEnds(float thickness, float position) { - surface.BeginFigure(); - - float height = thickness; - float top = position - (height * .5F); - float bottom = top + height; - Vector2 scale = new Vector2(scaledPoint) / this.ScaleFactor * MirrorScale; Vector2 offset = location + (this.offset * scale * MirrorScale); @@ -362,10 +356,24 @@ void DrawLine(float thickness, float position) width = this.LeftSideBearing + this.Width; } - Vector2 tl = (new Vector2(0, top) * scale) + offset; - Vector2 tr = (new Vector2(width, top) * scale) + offset; - Vector2 br = (new Vector2(width, bottom) * scale) + offset; - Vector2 bl = (new Vector2(0, bottom) * scale) + offset; + Vector2 tl = (new Vector2(0, position) * scale) + offset; + Vector2 tr = (new Vector2(width, position) * scale) + offset; + Vector2 bl = (new Vector2(0, position + thickness) * scale) + offset; + + return (tl, tr, tl.Y - bl.Y); + } + + void DrawLine(float thickness, float position) + { + surface.BeginFigure(); + + var (start, end, finalThickness) = GetEnds(thickness, position); + var halfHeight = new Vector2(0, -finalThickness * .5F); + + Vector2 tl = start - halfHeight; + Vector2 tr = end - halfHeight; + Vector2 bl = start + halfHeight; + Vector2 br = end + halfHeight; // Clamp the horizontal components to a whole pixel. tl.Y = MathF.Ceiling(tl.Y); @@ -387,17 +395,53 @@ void DrawLine(float thickness, float position) surface.EndFigure(); } - // Add underline and stroke. - // TextRun is never null here as rendering is only accessable via a Glyph which - // uses the cloned metrics instance. - if ((this.textRun!.TextAttributes & TextAttribute.Underline) == TextAttribute.Underline) + void SetDecoration(TextDecoration decorationType, float thickness, float position) { - DrawLine(this.FontMetrics.UnderlineThickness, this.FontMetrics.UnderlinePosition); + var (start, end, calcThickness) = GetEnds(thickness, position); + ((IGlyphDecorationRenderer)surface).SetDecoration(decorationType, start, end, calcThickness); } - if ((this.textRun!.TextAttributes & TextAttribute.Strikethrough) == TextAttribute.Strikethrough) + // we will need to infer this from the other metrics + // TODO figure out real place to get this metadata !!! + var virtualOverlineThickness = this.FontMetrics.UnderlineThickness; + var virtualOverlinePosition = this.FontMetrics.Ascender; + if (surface is IGlyphDecorationRenderer decorationSurface) + { + // allow the rendered to override the decorations to attach + var decorations = decorationSurface.EnabledDecorations(); + if ((decorations & TextDecoration.Underline) == TextDecoration.Underline) + { + SetDecoration(TextDecoration.Underline, this.FontMetrics.UnderlineThickness, this.FontMetrics.UnderlinePosition); + } + + if ((decorations & TextDecoration.Strikeout) == TextDecoration.Strikeout) + { + SetDecoration(TextDecoration.Strikeout, this.FontMetrics.StrikeoutSize, this.FontMetrics.StrikeoutPosition); + } + + if ((decorations & TextDecoration.Overline) == TextDecoration.Overline) + { + SetDecoration(TextDecoration.Overline, virtualOverlineThickness, virtualOverlinePosition); + } + } + else { - DrawLine(this.FontMetrics.StrikeoutSize, this.FontMetrics.StrikeoutPosition); + // TextRun is never null here as rendering is only accessable via a Glyph which + // uses the cloned metrics instance. + if ((this.textRun!.TextDecorations & TextDecoration.Underline) == TextDecoration.Underline) + { + DrawLine(this.FontMetrics.UnderlineThickness, this.FontMetrics.UnderlinePosition); + } + + if ((this.textRun!.TextDecorations & TextDecoration.Strikeout) == TextDecoration.Strikeout) + { + DrawLine(this.FontMetrics.StrikeoutSize, this.FontMetrics.StrikeoutPosition); + } + + if ((this.textRun!.TextDecorations & TextDecoration.Overline) == TextDecoration.Overline) + { + DrawLine(virtualOverlineThickness, virtualOverlinePosition); + } } } diff --git a/src/SixLabors.Fonts/IGlyphDecorationRenderer.cs b/src/SixLabors.Fonts/IGlyphDecorationRenderer.cs new file mode 100644 index 000000000..4f408737a --- /dev/null +++ b/src/SixLabors.Fonts/IGlyphDecorationRenderer.cs @@ -0,0 +1,22 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.Numerics; + +namespace SixLabors.Fonts +{ + /// + /// A surface that can have a glyph rendered to it as a series of actions with the options of providing glyph decoration details. + /// + public interface IGlyphDecorationRenderer : IGlyphRenderer + { + /// + /// provides a callback to enable custom logic to request decoration details. + /// a custom TextRun might use alternative triggers to determine what decorations it needs access to. + /// + /// The text decorations the render wants render info for. + public TextDecoration EnabledDecorations(); + + public void SetDecoration(TextDecoration textDecoration, Vector2 start, Vector2 end, float thickness); + } +} diff --git a/src/SixLabors.Fonts/TextAttribute.cs b/src/SixLabors.Fonts/TextAttribute.cs index a6ba01718..4309d54df 100644 --- a/src/SixLabors.Fonts/TextAttribute.cs +++ b/src/SixLabors.Fonts/TextAttribute.cs @@ -16,24 +16,14 @@ public enum TextAttribute /// None = 0, - /// - /// The text is underlined - /// - Underline = 1 << 0, - - /// - /// The text contains a horizontal line through the center. - /// - Strikethrough = 1 << 1, - /// /// The text set slightly below the normal line of type. /// - Subscript = 1 << 2, + Subscript = 1 << 0, /// /// The text set slightly above the normal line of type. /// - Superscript = 1 << 3, + Superscript = 1 << 1, } } diff --git a/src/SixLabors.Fonts/TextDecoration.cs b/src/SixLabors.Fonts/TextDecoration.cs new file mode 100644 index 000000000..1e80d6070 --- /dev/null +++ b/src/SixLabors.Fonts/TextDecoration.cs @@ -0,0 +1,34 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; + +namespace SixLabors.Fonts +{ + /// + /// Provides enumeration of various text decorations. + /// + [Flags] + public enum TextDecoration + { + /// + /// No attributes are applied + /// + None = 0, + + /// + /// The text is underlined + /// + Underline = 1 << 0, + + /// + /// The text contains a horizontal line through the center. + /// + Strikeout = 1 << 1, + + /// + /// The text contains a horizontal line above it + /// + Overline = 1 << 2 + } +} diff --git a/src/SixLabors.Fonts/TextRun.cs b/src/SixLabors.Fonts/TextRun.cs index 7b07e0d71..36de004ea 100644 --- a/src/SixLabors.Fonts/TextRun.cs +++ b/src/SixLabors.Fonts/TextRun.cs @@ -32,6 +32,11 @@ public class TextRun /// public TextAttribute TextAttributes { get; set; } + /// + /// Gets or sets the text decorations applied to this run. + /// + public TextDecoration TextDecorations { get; set; } + /// /// Returns the slice of the given text representing this . /// From 9889bde49276a6133cb19039ade9ac70c5b3d6a3 Mon Sep 17 00:00:00 2001 From: Scott Williams Date: Sun, 6 Mar 2022 21:26:30 +0000 Subject: [PATCH 33/48] stylecoped --- src/SixLabors.Fonts/GlyphMetrics.cs | 2 +- src/SixLabors.Fonts/IGlyphDecorationRenderer.cs | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/SixLabors.Fonts/GlyphMetrics.cs b/src/SixLabors.Fonts/GlyphMetrics.cs index afc912938..487686960 100644 --- a/src/SixLabors.Fonts/GlyphMetrics.cs +++ b/src/SixLabors.Fonts/GlyphMetrics.cs @@ -343,7 +343,7 @@ internal void RenderTo(IGlyphRenderer surface, float pointSize, Vector2 location } } - (Vector2 start, Vector2 end, float thickness) GetEnds(float thickness, float position) + (Vector2 Start, Vector2 End, float Thickness) GetEnds(float thickness, float position) { Vector2 scale = new Vector2(scaledPoint) / this.ScaleFactor * MirrorScale; Vector2 offset = location + (this.offset * scale * MirrorScale); diff --git a/src/SixLabors.Fonts/IGlyphDecorationRenderer.cs b/src/SixLabors.Fonts/IGlyphDecorationRenderer.cs index 4f408737a..839df7764 100644 --- a/src/SixLabors.Fonts/IGlyphDecorationRenderer.cs +++ b/src/SixLabors.Fonts/IGlyphDecorationRenderer.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. using System.Numerics; @@ -17,6 +17,13 @@ public interface IGlyphDecorationRenderer : IGlyphRenderer /// The text decorations the render wants render info for. public TextDecoration EnabledDecorations(); + /// + /// Provides the positions required for drawing text decorations onto the + /// + /// The type of decoration these details correspond to. + /// The start position from where to draw the decoration from + /// The end position from where to draw the decoration to + /// The thickness to draw the decoration. public void SetDecoration(TextDecoration textDecoration, Vector2 start, Vector2 end, float thickness); } } From 8360ce7728c82477e5b8c31c3e9a1a11249d7702 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 7 Mar 2022 11:42:08 +1100 Subject: [PATCH 34/48] Offset overline to avoid clipping. --- src/SixLabors.Fonts/GlyphMetrics.cs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/SixLabors.Fonts/GlyphMetrics.cs b/src/SixLabors.Fonts/GlyphMetrics.cs index 487686960..28c3729b0 100644 --- a/src/SixLabors.Fonts/GlyphMetrics.cs +++ b/src/SixLabors.Fonts/GlyphMetrics.cs @@ -350,7 +350,7 @@ internal void RenderTo(IGlyphRenderer surface, float pointSize, Vector2 location // Calculate the correct advance for the line. float width = this.AdvanceWidth; - if (this.AdvanceWidth == 0) + if (width == 0) { // For zero advance glyphs we must calculate our advance width from bearing + width; width = this.LeftSideBearing + this.Width; @@ -367,7 +367,7 @@ void DrawLine(float thickness, float position) { surface.BeginFigure(); - var (start, end, finalThickness) = GetEnds(thickness, position); + (Vector2 start, Vector2 end, float finalThickness) = GetEnds(thickness, position); var halfHeight = new Vector2(0, -finalThickness * .5F); Vector2 tl = start - halfHeight; @@ -397,18 +397,18 @@ void DrawLine(float thickness, float position) void SetDecoration(TextDecoration decorationType, float thickness, float position) { - var (start, end, calcThickness) = GetEnds(thickness, position); + (Vector2 start, Vector2 end, float calcThickness) = GetEnds(thickness, position); ((IGlyphDecorationRenderer)surface).SetDecoration(decorationType, start, end, calcThickness); } - // we will need to infer this from the other metrics - // TODO figure out real place to get this metadata !!! - var virtualOverlineThickness = this.FontMetrics.UnderlineThickness; - var virtualOverlinePosition = this.FontMetrics.Ascender; + // There's no built in metrics for these values so we will need to infer them from the other metrics. + // Offset to avoid clipping. + float overlineThickness = this.FontMetrics.UnderlineThickness; + float overlinePosition = this.FontMetrics.Ascender - (overlineThickness * .5F); if (surface is IGlyphDecorationRenderer decorationSurface) { // allow the rendered to override the decorations to attach - var decorations = decorationSurface.EnabledDecorations(); + TextDecoration decorations = decorationSurface.EnabledDecorations(); if ((decorations & TextDecoration.Underline) == TextDecoration.Underline) { SetDecoration(TextDecoration.Underline, this.FontMetrics.UnderlineThickness, this.FontMetrics.UnderlinePosition); @@ -421,7 +421,7 @@ void SetDecoration(TextDecoration decorationType, float thickness, float positio if ((decorations & TextDecoration.Overline) == TextDecoration.Overline) { - SetDecoration(TextDecoration.Overline, virtualOverlineThickness, virtualOverlinePosition); + SetDecoration(TextDecoration.Overline, overlineThickness, overlinePosition); } } else @@ -440,7 +440,7 @@ void SetDecoration(TextDecoration decorationType, float thickness, float positio if ((this.textRun!.TextDecorations & TextDecoration.Overline) == TextDecoration.Overline) { - DrawLine(virtualOverlineThickness, virtualOverlinePosition); + DrawLine(overlineThickness, overlinePosition); } } } From 847c668c74931ce18b25087313ec04eaccbe439a Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 7 Mar 2022 11:42:33 +1100 Subject: [PATCH 35/48] Fixed fix (ha!) --- src/SixLabors.Fonts/BigEndianBinaryReader.cs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/SixLabors.Fonts/BigEndianBinaryReader.cs b/src/SixLabors.Fonts/BigEndianBinaryReader.cs index 4a7288ca8..39595a2f7 100644 --- a/src/SixLabors.Fonts/BigEndianBinaryReader.cs +++ b/src/SixLabors.Fonts/BigEndianBinaryReader.cs @@ -112,17 +112,14 @@ public TEnum ReadInt16() public ushort ReadUFWORD() => this.ReadUInt16(); /// - /// Reads a 32-bit signed integer from the stream. + /// Reads a fixed 32-bit value from the stream. /// 4 bytes are read. /// - /// The 32-bit integer read. + /// The 32-bit value read. public float ReadFixed() { this.ReadInternal(this.buffer, 4); - - int value = BinaryPrimitives.ReadInt32BigEndian(this.buffer); - - return Unsafe.As(ref value); + return BinaryPrimitives.ReadInt32BigEndian(this.buffer) / 65536F; } /// From 1303b7c9abab4d842b0f1598a47ffa6171564c7d Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 7 Mar 2022 12:36:19 +1100 Subject: [PATCH 36/48] Add font metrics tests for new properties --- samples/DrawWithImageSharp/Program.cs | 4 +- .../SixLabors.Fonts.Tests/FontMetricsTests.cs | 42 ++++++++++++++++++- 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/samples/DrawWithImageSharp/Program.cs b/samples/DrawWithImageSharp/Program.cs index cb16d156d..766d0471b 100644 --- a/samples/DrawWithImageSharp/Program.cs +++ b/samples/DrawWithImageSharp/Program.cs @@ -44,8 +44,8 @@ public static void Main(string[] args) FontFamily tahoma = SystemFonts.Get("Tahoma"); var textRuns = new List { - new TextRun { Start = 4, End = 9, Font = uiFont.CreateFont(72, FontStyle.Bold), TextAttributes = TextAttribute.Superscript, TextDecorations = TextDecoration.Strikeout }, - new TextRun { Start = 26, End = 30, Font = uiFont.CreateFont(72, FontStyle.Italic), TextAttributes = TextAttribute.Subscript, TextDecorations = TextDecoration.Underline } + new TextRun { Start = 4, End = 9, Font = uiFont.CreateFont(72, FontStyle.Bold), TextAttributes = TextAttribute.Superscript, TextDecorations = TextDecoration.Underline | TextDecoration.Strikeout | TextDecoration.Overline }, + new TextRun { Start = 26, End = 30, Font = uiFont.CreateFont(72, FontStyle.Italic), TextAttributes = TextAttribute.Subscript, TextDecorations = TextDecoration.Strikeout | TextDecoration.Underline | TextDecoration.Overline } }; RenderText(uiFont, "The quick brown fox jumps over the lazy dog", pointSize: 72, textRuns: textRuns); diff --git a/tests/SixLabors.Fonts.Tests/FontMetricsTests.cs b/tests/SixLabors.Fonts.Tests/FontMetricsTests.cs index 45635a93e..ae454a024 100644 --- a/tests/SixLabors.Fonts.Tests/FontMetricsTests.cs +++ b/tests/SixLabors.Fonts.Tests/FontMetricsTests.cs @@ -13,7 +13,8 @@ public class FontMetricsTests [Fact] public void FontMetricsMatchesReference() { - // Compared to FontForge metrics. + // Compared to EveryFonts TTFDump metrics + // https://everythingfonts.com/ttfdump var collection = new FontCollection(); FontFamily family = collection.Add(TestFonts.OpenSansFile); Font font = family.CreateFont(12); @@ -26,6 +27,24 @@ public void FontMetricsMatchesReference() Assert.Equal(2470, font.FontMetrics.AdvanceWidthMax); Assert.Equal(font.FontMetrics.LineHeight, font.FontMetrics.AdvanceHeightMax); + Assert.Equal(1331, font.FontMetrics.SubscriptXSize); + Assert.Equal(1229, font.FontMetrics.SubscriptYSize); + Assert.Equal(0, font.FontMetrics.SubscriptXOffset); + Assert.Equal(154, font.FontMetrics.SubscriptYOffset); + + Assert.Equal(1331, font.FontMetrics.SuperscriptXSize); + Assert.Equal(1229, font.FontMetrics.SuperscriptYSize); + Assert.Equal(0, font.FontMetrics.SuperscriptXOffset); + Assert.Equal(717, font.FontMetrics.SuperscriptYOffset); + + Assert.Equal(50, font.FontMetrics.StrikeoutSize); + Assert.Equal(658, font.FontMetrics.StrikeoutPosition); + + Assert.Equal(-100, font.FontMetrics.UnderlinePosition); + Assert.Equal(50, font.FontMetrics.UnderlineThickness); + + Assert.Equal(0, font.FontMetrics.ItalicAngle); + Assert.False(font.IsBold); Assert.False(font.IsItalic); } @@ -47,6 +66,24 @@ public void FontMetricsVerticalFontMatchesReference() Assert.Equal(1000, font.FontMetrics.AdvanceWidthMax); Assert.Equal(1000, font.FontMetrics.AdvanceHeightMax); + Assert.Equal(650, font.FontMetrics.SubscriptXSize); + Assert.Equal(700, font.FontMetrics.SubscriptYSize); + Assert.Equal(0, font.FontMetrics.SubscriptXOffset); + Assert.Equal(140, font.FontMetrics.SubscriptYOffset); + + Assert.Equal(650, font.FontMetrics.SuperscriptXSize); + Assert.Equal(700, font.FontMetrics.SuperscriptYSize); + Assert.Equal(0, font.FontMetrics.SuperscriptXOffset); + Assert.Equal(480, font.FontMetrics.SuperscriptYOffset); + + Assert.Equal(49, font.FontMetrics.StrikeoutSize); + Assert.Equal(258, font.FontMetrics.StrikeoutPosition); + + Assert.Equal(-75, font.FontMetrics.UnderlinePosition); + Assert.Equal(50, font.FontMetrics.UnderlineThickness); + + Assert.Equal(0, font.FontMetrics.ItalicAngle); + Assert.False(font.IsBold); Assert.False(font.IsItalic); } @@ -54,7 +91,8 @@ public void FontMetricsVerticalFontMatchesReference() [Fact] public void GlyphMetricsMatchesReference() { - // Compared to FontForge metrics + // Compared to EveryFonts TTFDump metrics + // https://everythingfonts.com/ttfdump var collection = new FontCollection(); FontFamily family = collection.Add(TestFonts.OpenSansFile); Font font = family.CreateFont(12); From 5d8fdfe91d0a554f58c29f8ec371871a27391147 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 7 Mar 2022 13:16:13 +1100 Subject: [PATCH 37/48] Update enum names to match runtime standard --- samples/DrawWithImageSharp/Program.cs | 8 +++--- src/SixLabors.Fonts/Font.cs | 4 +-- src/SixLabors.Fonts/GlyphMetrics.cs | 26 +++++++++---------- .../IGlyphDecorationRenderer.cs | 12 ++++----- .../{TextAttribute.cs => TextAttributes.cs} | 2 +- .../{TextDecoration.cs => TextDecorations.cs} | 4 +-- src/SixLabors.Fonts/TextRun.cs | 4 +-- 7 files changed, 30 insertions(+), 30 deletions(-) rename src/SixLabors.Fonts/{TextAttribute.cs => TextAttributes.cs} (95%) rename src/SixLabors.Fonts/{TextDecoration.cs => TextDecorations.cs} (91%) diff --git a/samples/DrawWithImageSharp/Program.cs b/samples/DrawWithImageSharp/Program.cs index 766d0471b..85730cf2c 100644 --- a/samples/DrawWithImageSharp/Program.cs +++ b/samples/DrawWithImageSharp/Program.cs @@ -44,8 +44,8 @@ public static void Main(string[] args) FontFamily tahoma = SystemFonts.Get("Tahoma"); var textRuns = new List { - new TextRun { Start = 4, End = 9, Font = uiFont.CreateFont(72, FontStyle.Bold), TextAttributes = TextAttribute.Superscript, TextDecorations = TextDecoration.Underline | TextDecoration.Strikeout | TextDecoration.Overline }, - new TextRun { Start = 26, End = 30, Font = uiFont.CreateFont(72, FontStyle.Italic), TextAttributes = TextAttribute.Subscript, TextDecorations = TextDecoration.Strikeout | TextDecoration.Underline | TextDecoration.Overline } + new TextRun { Start = 4, End = 9, Font = uiFont.CreateFont(72, FontStyle.Bold), TextAttributes = TextAttributes.Superscript, TextDecorations = TextDecorations.Underline | TextDecorations.Strikeout | TextDecorations.Overline }, + new TextRun { Start = 26, End = 30, Font = uiFont.CreateFont(72, FontStyle.Italic), TextAttributes = TextAttributes.Subscript, TextDecorations = TextDecorations.Strikeout | TextDecorations.Underline | TextDecorations.Overline } }; RenderText(uiFont, "The quick brown fox jumps over the lazy dog", pointSize: 72, textRuns: textRuns); @@ -53,14 +53,14 @@ public static void Main(string[] args) string arabic = "بِسْمِ ٱللَّهِ ٱلرَّحْمَٟنِ ٱلرَّحِيمِ"; textRuns = new List { - new TextRun { Start = 0, End = CodePoint.GetCodePointCount(arabic), TextDecorations = TextDecoration.Underline } + new TextRun { Start = 0, End = CodePoint.GetCodePointCount(arabic), TextDecorations = TextDecorations.Underline } }; RenderText(uiFont, arabic, pointSize: 72, textRuns: textRuns); textRuns = new List { - new TextRun { Start = 0, End = CodePoint.GetCodePointCount(arabic), TextDecorations = TextDecoration.Underline } + new TextRun { Start = 0, End = CodePoint.GetCodePointCount(arabic), TextDecorations = TextDecorations.Underline } }; RenderText(bugzilla, arabic, pointSize: 72, textRuns: textRuns); diff --git a/src/SixLabors.Fonts/Font.cs b/src/SixLabors.Fonts/Font.cs index 5a9fa1f3f..31a9b2fbc 100644 --- a/src/SixLabors.Fonts/Font.cs +++ b/src/SixLabors.Fonts/Font.cs @@ -149,7 +149,7 @@ public bool TryGetPath([NotNullWhen(true)] out string? path) /// Options for enabling color font support during layout and rendering. /// Returns the glyph public IEnumerable GetGlyphs(CodePoint codePoint, ColorFontSupport support) - => this.GetGlyphs(codePoint, TextAttribute.None, support); + => this.GetGlyphs(codePoint, TextAttributes.None, support); /// /// Gets the glyphs for the given codepoint. @@ -158,7 +158,7 @@ public IEnumerable GetGlyphs(CodePoint codePoint, ColorFontSupport suppor /// The text attributes to apply to the glyphs. /// Options for enabling color font support during layout and rendering. /// Returns the glyph - public IEnumerable GetGlyphs(CodePoint codePoint, TextAttribute textAttributes, ColorFontSupport support) + public IEnumerable GetGlyphs(CodePoint codePoint, TextAttributes textAttributes, ColorFontSupport support) { TextRun textRun = new() { Start = 0, End = 1, Font = this, TextAttributes = textAttributes }; foreach (GlyphMetrics metrics in this.FontMetrics.GetGlyphMetrics(codePoint, support)) diff --git a/src/SixLabors.Fonts/GlyphMetrics.cs b/src/SixLabors.Fonts/GlyphMetrics.cs index 28c3729b0..8d5c1ac1e 100644 --- a/src/SixLabors.Fonts/GlyphMetrics.cs +++ b/src/SixLabors.Fonts/GlyphMetrics.cs @@ -138,13 +138,13 @@ internal static GlyphMetrics CloneForRendering(GlyphMetrics other, TextRun textR StreamFontMetrics fontMetrics = other.FontMetrics; Vector2 offset = other.offset; Vector2 scaleFactor = other.ScaleFactor; - if (textRun.TextAttributes.HasFlag(TextAttribute.Subscript)) + if (textRun.TextAttributes.HasFlag(TextAttributes.Subscript)) { float units = other.UnitsPerEm; scaleFactor /= new Vector2(fontMetrics.SubscriptXSize / units, fontMetrics.SubscriptYSize / units); offset = new(other.FontMetrics.SubscriptXOffset, other.FontMetrics.SubscriptYOffset); } - else if (textRun.TextAttributes.HasFlag(TextAttribute.Superscript)) + else if (textRun.TextAttributes.HasFlag(TextAttributes.Superscript)) { float units = other.UnitsPerEm; scaleFactor /= new Vector2(fontMetrics.SuperscriptXSize / units, fontMetrics.SuperscriptYSize / units); @@ -395,7 +395,7 @@ void DrawLine(float thickness, float position) surface.EndFigure(); } - void SetDecoration(TextDecoration decorationType, float thickness, float position) + void SetDecoration(TextDecorations decorationType, float thickness, float position) { (Vector2 start, Vector2 end, float calcThickness) = GetEnds(thickness, position); ((IGlyphDecorationRenderer)surface).SetDecoration(decorationType, start, end, calcThickness); @@ -408,37 +408,37 @@ void SetDecoration(TextDecoration decorationType, float thickness, float positio if (surface is IGlyphDecorationRenderer decorationSurface) { // allow the rendered to override the decorations to attach - TextDecoration decorations = decorationSurface.EnabledDecorations(); - if ((decorations & TextDecoration.Underline) == TextDecoration.Underline) + TextDecorations decorations = decorationSurface.EnabledDecorations(); + if ((decorations & TextDecorations.Underline) == TextDecorations.Underline) { - SetDecoration(TextDecoration.Underline, this.FontMetrics.UnderlineThickness, this.FontMetrics.UnderlinePosition); + SetDecoration(TextDecorations.Underline, this.FontMetrics.UnderlineThickness, this.FontMetrics.UnderlinePosition); } - if ((decorations & TextDecoration.Strikeout) == TextDecoration.Strikeout) + if ((decorations & TextDecorations.Strikeout) == TextDecorations.Strikeout) { - SetDecoration(TextDecoration.Strikeout, this.FontMetrics.StrikeoutSize, this.FontMetrics.StrikeoutPosition); + SetDecoration(TextDecorations.Strikeout, this.FontMetrics.StrikeoutSize, this.FontMetrics.StrikeoutPosition); } - if ((decorations & TextDecoration.Overline) == TextDecoration.Overline) + if ((decorations & TextDecorations.Overline) == TextDecorations.Overline) { - SetDecoration(TextDecoration.Overline, overlineThickness, overlinePosition); + SetDecoration(TextDecorations.Overline, overlineThickness, overlinePosition); } } else { // TextRun is never null here as rendering is only accessable via a Glyph which // uses the cloned metrics instance. - if ((this.textRun!.TextDecorations & TextDecoration.Underline) == TextDecoration.Underline) + if ((this.textRun!.TextDecorations & TextDecorations.Underline) == TextDecorations.Underline) { DrawLine(this.FontMetrics.UnderlineThickness, this.FontMetrics.UnderlinePosition); } - if ((this.textRun!.TextDecorations & TextDecoration.Strikeout) == TextDecoration.Strikeout) + if ((this.textRun!.TextDecorations & TextDecorations.Strikeout) == TextDecorations.Strikeout) { DrawLine(this.FontMetrics.StrikeoutSize, this.FontMetrics.StrikeoutPosition); } - if ((this.textRun!.TextDecorations & TextDecoration.Overline) == TextDecoration.Overline) + if ((this.textRun!.TextDecorations & TextDecorations.Overline) == TextDecorations.Overline) { DrawLine(overlineThickness, overlinePosition); } diff --git a/src/SixLabors.Fonts/IGlyphDecorationRenderer.cs b/src/SixLabors.Fonts/IGlyphDecorationRenderer.cs index 839df7764..14465154a 100644 --- a/src/SixLabors.Fonts/IGlyphDecorationRenderer.cs +++ b/src/SixLabors.Fonts/IGlyphDecorationRenderer.cs @@ -11,19 +11,19 @@ namespace SixLabors.Fonts public interface IGlyphDecorationRenderer : IGlyphRenderer { /// - /// provides a callback to enable custom logic to request decoration details. + /// Provides a callback to enable custom logic to request decoration details. /// a custom TextRun might use alternative triggers to determine what decorations it needs access to. /// /// The text decorations the render wants render info for. - public TextDecoration EnabledDecorations(); + public TextDecorations EnabledDecorations(); /// /// Provides the positions required for drawing text decorations onto the /// - /// The type of decoration these details correspond to. - /// The start position from where to draw the decoration from - /// The end position from where to draw the decoration to + /// The type of decoration these details correspond to. + /// The start position from where to draw the decorations from. + /// The end position from where to draw the decorations to. /// The thickness to draw the decoration. - public void SetDecoration(TextDecoration textDecoration, Vector2 start, Vector2 end, float thickness); + public void SetDecoration(TextDecorations textDecorations, Vector2 start, Vector2 end, float thickness); } } diff --git a/src/SixLabors.Fonts/TextAttribute.cs b/src/SixLabors.Fonts/TextAttributes.cs similarity index 95% rename from src/SixLabors.Fonts/TextAttribute.cs rename to src/SixLabors.Fonts/TextAttributes.cs index 4309d54df..310560df7 100644 --- a/src/SixLabors.Fonts/TextAttribute.cs +++ b/src/SixLabors.Fonts/TextAttributes.cs @@ -9,7 +9,7 @@ namespace SixLabors.Fonts /// Provides enumeration of various text attributes. /// [Flags] - public enum TextAttribute + public enum TextAttributes { /// /// No attributes are applied diff --git a/src/SixLabors.Fonts/TextDecoration.cs b/src/SixLabors.Fonts/TextDecorations.cs similarity index 91% rename from src/SixLabors.Fonts/TextDecoration.cs rename to src/SixLabors.Fonts/TextDecorations.cs index 1e80d6070..cc84b3007 100644 --- a/src/SixLabors.Fonts/TextDecoration.cs +++ b/src/SixLabors.Fonts/TextDecorations.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. using System; @@ -9,7 +9,7 @@ namespace SixLabors.Fonts /// Provides enumeration of various text decorations. /// [Flags] - public enum TextDecoration + public enum TextDecorations { /// /// No attributes are applied diff --git a/src/SixLabors.Fonts/TextRun.cs b/src/SixLabors.Fonts/TextRun.cs index 36de004ea..ba3c107ab 100644 --- a/src/SixLabors.Fonts/TextRun.cs +++ b/src/SixLabors.Fonts/TextRun.cs @@ -30,12 +30,12 @@ public class TextRun /// /// Gets or sets the text attributes applied to this run. /// - public TextAttribute TextAttributes { get; set; } + public TextAttributes TextAttributes { get; set; } /// /// Gets or sets the text decorations applied to this run. /// - public TextDecoration TextDecorations { get; set; } + public TextDecorations TextDecorations { get; set; } /// /// Returns the slice of the given text representing this . From 642999d8d817721a984fe6ea5e5f9f65652e2cf7 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 11 Mar 2022 13:51:18 +1100 Subject: [PATCH 38/48] Fix horizontal variable font size line-height handling --- src/SixLabors.Fonts/GlyphLayout.cs | 33 +++- src/SixLabors.Fonts/TextLayout.cs | 265 ++++++++++++++++++---------- src/SixLabors.Fonts/TextMeasurer.cs | 15 +- 3 files changed, 203 insertions(+), 110 deletions(-) diff --git a/src/SixLabors.Fonts/GlyphLayout.cs b/src/SixLabors.Fonts/GlyphLayout.cs index f82392aa8..21fe94d7f 100644 --- a/src/SixLabors.Fonts/GlyphLayout.cs +++ b/src/SixLabors.Fonts/GlyphLayout.cs @@ -14,15 +14,21 @@ internal readonly struct GlyphLayout internal GlyphLayout( Glyph glyph, Vector2 location, + float ascender, + float descender, + float linegap, + float lineHeight, float width, float height, - float lineHeight, bool isStartOfLine) { - this.LineHeight = lineHeight; this.Glyph = glyph; this.CodePoint = glyph.GlyphMetrics.CodePoint; this.Location = location; + this.Ascender = ascender; + this.Descender = descender; + this.LineGap = linegap; + this.LineHeight = lineHeight; this.Width = width; this.Height = height; this.IsStartOfLine = isStartOfLine; @@ -44,20 +50,35 @@ internal GlyphLayout( public Vector2 Location { get; } /// - /// Gets the width. + /// Gets the ascender /// - public float Width { get; } + public float Ascender { get; } /// - /// Gets the height. + /// Gets the ascender /// - public float Height { get; } + public float Descender { get; } + + /// + /// Gets the lie gap + /// + public float LineGap { get; } /// /// Gets the line height of the glyph. /// public float LineHeight { get; } + /// + /// Gets the width. + /// + public float Width { get; } + + /// + /// Gets the height. + /// + public float Height { get; } + /// /// Gets a value indicating whether this glyph is the first glyph on a new line. /// diff --git a/src/SixLabors.Fonts/TextLayout.cs b/src/SixLabors.Fonts/TextLayout.cs index 6c677d9f2..908dfbd47 100644 --- a/src/SixLabors.Fonts/TextLayout.cs +++ b/src/SixLabors.Fonts/TextLayout.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Collections.ObjectModel; using System.Diagnostics; using System.Linq; using System.Numerics; @@ -203,21 +202,22 @@ private static IReadOnlyList LayoutText(TextBox textBox, TextOption LayoutMode layoutMode = options.LayoutMode; List glyphs = new(); - Vector2 location = options.Origin / options.Dpi; + Vector2 origin = options.Origin / options.Dpi; + Vector2 location = origin; float maxScaledAdvance = textBox.TextLines.Max(x => x.ScaledLineAdvance()); TextDirection direction = textBox.TextDirection(); if (layoutMode == LayoutMode.HorizontalTopBottom) { for (int i = 0; i < textBox.TextLines.Count; i++) { - glyphs.AddRange(LayoutLineHorizontal(textBox, textBox.TextLines[i], direction, maxScaledAdvance, options, glyphs.Count == 0, ref location)); + glyphs.AddRange(LayoutLineHorizontal(textBox, textBox.TextLines[i], direction, maxScaledAdvance, options, i, ref location, origin)); } } else if (layoutMode == LayoutMode.HorizontalBottomTop) { for (int i = textBox.TextLines.Count - 1; i >= 0; i--) { - glyphs.AddRange(LayoutLineHorizontal(textBox, textBox.TextLines[i], direction, maxScaledAdvance, options, glyphs.Count == 0, ref location)); + glyphs.AddRange(LayoutLineHorizontal(textBox, textBox.TextLines[i], direction, maxScaledAdvance, options, i, ref location, origin)); } } else if (layoutMode == LayoutMode.VerticalLeftRight) @@ -244,35 +244,62 @@ private static IEnumerable LayoutLineHorizontal( TextDirection direction, float maxScaledAdvance, TextOptions options, - bool first, - ref Vector2 location) + int index, + ref Vector2 location, + Vector2 origin) { + float scaledMaxLineGap = textLine.ScaledMaxLineGap(); + float scaledMaxAscender = textLine.ScaledMaxAscender(); + float scaledMaxDescender = textLine.ScaledMaxDescender(); + float scaledMaxLineHeight = textLine.ScaledMaxLineHeight(); + float scaledLineAdvance = scaledMaxDescender * options.LineSpacing; float originX = location.X; float offsetY = 0; float offsetX = 0; - if (first) + + // Set the Y-Origin for the line. + switch (options.VerticalAlignment) { - // Set the Y-Origin for the first line. - float scaledMaxAscender = textBox.ScaledMaxAscender + textBox.ScaledMaxLineGap; - float scaledMaxDescender = textBox.ScaledMaxDescender; - switch (options.VerticalAlignment) - { - case VerticalAlignment.Top: - offsetY = scaledMaxAscender; - break; - case VerticalAlignment.Center: - offsetY = (scaledMaxAscender * .5F) - (scaledMaxDescender * .5F); - offsetY -= (textBox.TextLines.Count - 1) * textBox.ScaledMaxLineHeight * options.LineSpacing * .5F; - break; - case VerticalAlignment.Bottom: - offsetY = -scaledMaxDescender; - offsetY -= (textBox.TextLines.Count - 1) * textBox.ScaledMaxLineHeight * options.LineSpacing; - break; - } + case VerticalAlignment.Top: + offsetY = scaledMaxAscender + scaledMaxLineGap; + break; + case VerticalAlignment.Center: + location.Y = origin.Y; + offsetY = ((scaledMaxAscender + scaledMaxLineGap) * .5F) - (scaledMaxDescender * .5F); + + // Vertically centering aligning multiple lines of variable line hight is tricky. + if (textBox.TextLines.Count > 1) + { + // First negatively offset each line based on its index aligning at the bottom + // as we do with the bottom alignment below. + for (int i = index; i < textBox.TextLines.Count; i++) + { + offsetY -= textBox.TextLines[i].ScaledMaxLineHeight(); + } - location.Y += offsetY; + // Now push all the lines back down by half. + // The textline methods are memoized so we're safe to call them multiple times. + for (int i = 0; i < textBox.TextLines.Count; i++) + { + TextLine current = textBox.TextLines[i]; + offsetY += (current.ScaledMaxLineHeight() + (current.ScaledMaxDescender() * options.LineSpacing)) * .5F; + } + } + + break; + case VerticalAlignment.Bottom: + location.Y = origin.Y; + offsetY = -scaledMaxDescender; + for (int i = index; i < textBox.TextLines.Count - 1; i++) + { + offsetY -= textBox.TextLines[i].ScaledMaxLineHeight(); + } + + break; } + location.Y += offsetY; + // Set the X-Origin for horizontal alignment. switch (options.HorizontalAlignment) { @@ -318,7 +345,7 @@ private static IEnumerable LayoutLineHorizontal( TextLine.GlyphLayoutData info = textLine[i]; if (info.IsNewLine) { - location.Y += textBox.ScaledMaxLineHeight * options.LineSpacing; + location.Y += scaledLineAdvance; continue; } @@ -341,9 +368,12 @@ private static IEnumerable LayoutLineHorizontal( glyphs.Add(new GlyphLayout( new Glyph(metric, info.PointSize), location, + scaledMaxAscender, + scaledMaxDescender, + scaledMaxLineGap, + scaledMaxLineHeight, advanceX, advanceY, - textBox.ScaledMaxLineHeight * options.LineSpacing, i == 0)); } @@ -353,7 +383,7 @@ private static IEnumerable LayoutLineHorizontal( location.X = originX; if (glyphs.Count > 0) { - location.Y += textBox.ScaledMaxLineHeight * options.LineSpacing; + location.Y += scaledLineAdvance; } return glyphs; @@ -372,9 +402,12 @@ private static IEnumerable LayoutLineVertical( float offsetY = 0; float offsetX = 0; - // Set the Y-Origin for the first line. - float scaledMaxAscender = textBox.ScaledMaxAscender + textBox.ScaledMaxLineGap; - float scaledMaxDescender = textBox.ScaledMaxDescender; + // Set the Y-Origin for the line. + float scaledMaxLineGap = textLine.ScaledMaxLineGap(); + float scaledMaxAscender = textLine.ScaledMaxAscender() + scaledMaxLineGap; + float scaledMaxDescender = textLine.ScaledMaxDescender(); + float scaledMaxLineHeight = textLine.ScaledMaxLineHeight(); + switch (options.VerticalAlignment) { case VerticalAlignment.Top: @@ -382,11 +415,11 @@ private static IEnumerable LayoutLineVertical( break; case VerticalAlignment.Center: offsetY = (scaledMaxAscender * .5F) - (scaledMaxDescender * .5F); - offsetY -= (maxScaledAdvance - textBox.ScaledMaxLineHeight) * .5F; + offsetY -= (maxScaledAdvance - scaledMaxLineHeight) * .5F; break; case VerticalAlignment.Bottom: offsetY = -scaledMaxDescender; - offsetY -= maxScaledAdvance - textBox.ScaledMaxLineHeight; + offsetY -= maxScaledAdvance - scaledMaxLineHeight; break; } @@ -424,12 +457,20 @@ private static IEnumerable LayoutLineVertical( switch (options.HorizontalAlignment) { case HorizontalAlignment.Right: - offsetX = -(textBox.ScaledMaxLineHeight * options.LineSpacing); - offsetX -= (textBox.TextLines.Count - 1) * textBox.ScaledMaxLineHeight * options.LineSpacing; + offsetX = -(scaledMaxLineHeight * options.LineSpacing); + for (int i = textBox.TextLines.Count - 1; i > 0; i--) + { + offsetX -= textBox.TextLines[i].ScaledMaxLineHeight() * options.LineSpacing; + } + break; case HorizontalAlignment.Center: - offsetX = -(textBox.ScaledMaxLineHeight * options.LineSpacing * .5F); - offsetX -= (textBox.TextLines.Count - 1) * textBox.ScaledMaxLineHeight * options.LineSpacing * .5F; + offsetX = -(scaledMaxLineHeight * options.LineSpacing * .5F); + for (int i = textBox.TextLines.Count - 1; i > 0; i--) + { + offsetX -= textBox.TextLines[i].ScaledMaxLineHeight() * options.LineSpacing * .5F; + } + break; } } @@ -437,12 +478,12 @@ private static IEnumerable LayoutLineVertical( location.X += offsetX; List glyphs = new(); - float xWidth = textBox.ScaledMaxLineHeight * (first ? 1F : options.LineSpacing); - float xLineAdvance = textBox.ScaledMaxLineHeight * options.LineSpacing; + float xWidth = scaledMaxLineHeight * (first ? 1F : options.LineSpacing); + float xLineAdvance = scaledMaxLineHeight * options.LineSpacing; if (first) { - xLineAdvance -= (xLineAdvance - textBox.ScaledMaxLineHeight) * .5F; + xLineAdvance -= (xLineAdvance - scaledMaxLineHeight) * .5F; } for (int i = 0; i < textLine.Count; i++) @@ -475,9 +516,12 @@ private static IEnumerable LayoutLineVertical( glyphs.Add(new GlyphLayout( new Glyph(metric, info.PointSize), location + new Vector2((xWidth - (metric.AdvanceWidth * scale.X)) * .5F, 0), + scaledMaxAscender, + scaledMaxDescender, + scaledMaxLineGap, + scaledMaxLineHeight, advanceX, advanceY, - textBox.ScaledMaxLineHeight, i == 0)); } @@ -628,11 +672,6 @@ private static TextBox BreakLines( bool keepAll = options.WordBreaking == WordBreaking.KeepAll; bool isHorizontal = !layoutMode.IsVertical(); - float scaledMaxAscender = 0; - float scaledMaxDescender = 0; - float scaledMaxLineHeight = 0; - float scaledMaxLeftSideBearing = 0; - // Calculate the position of potential line breaks. var lineBreakEnumerator = new LineBreakEnumerator(text); List lineBreaks = new(); @@ -809,30 +848,10 @@ private static TextBox BreakLines( } GlyphMetrics metric = metrics[0]; - float ascender = metric.FontMetrics.Ascender * pointSize / metric.ScaleFactor.Y; - float descender = Math.Abs(metric.FontMetrics.Descender * pointSize / metric.ScaleFactor.Y); - float lineHeight = metric.FontMetrics.LineHeight * pointSize / metric.ScaleFactor.Y; - float leftSideBearing = metric.LeftSideBearing * pointSize / metric.ScaleFactor.X; - - if (ascender > scaledMaxAscender) - { - scaledMaxAscender = ascender; - } - - if (descender > scaledMaxDescender) - { - scaledMaxDescender = descender; - } - - if (lineHeight > scaledMaxLineHeight) - { - scaledMaxLineHeight = lineHeight; - } - - if (leftSideBearing > scaledMaxLeftSideBearing) - { - scaledMaxLeftSideBearing = leftSideBearing; - } + float scaleY = pointSize / metric.ScaleFactor.Y; + float ascender = metric.FontMetrics.Ascender * scaleY; + float descender = Math.Abs(metric.FontMetrics.Descender * scaleY); + float lineHeight = metric.FontMetrics.LineHeight * scaleY; // Add our metrics to the line. lineAdvance += glyphAdvance; @@ -840,6 +859,9 @@ private static TextBox BreakLines( metrics, pointSize, glyphAdvance, + lineHeight, + ascender, + descender, bidiRuns[bidiMap[codePointIndex]], graphemeIndex, codePointIndex); @@ -855,37 +877,14 @@ private static TextBox BreakLines( textLines.Add(textLine.BidiReOrder()); } - return new TextBox(textLines, scaledMaxAscender, scaledMaxDescender, scaledMaxLineHeight, scaledMaxLeftSideBearing); + return new TextBox(textLines); } internal sealed class TextBox { - public TextBox( - IList textLines, - float scaledMaxAscender, - float scaledMaxDescender, - float scaledMaxLineHeight, - float scaledMaxLeftSideBearing) - { - this.TextLines = new(textLines); - this.ScaledMaxAscender = scaledMaxAscender; - this.ScaledMaxDescender = scaledMaxDescender; - this.ScaledMaxLineHeight = scaledMaxLineHeight; - this.ScaledMaxLineGap = scaledMaxLineHeight - (scaledMaxAscender + scaledMaxDescender); - this.ScaledMaxLeftSideBearing = scaledMaxLeftSideBearing; - } - - public float ScaledMaxAscender { get; } - - public float ScaledMaxDescender { get; } - - public float ScaledMaxLineHeight { get; } - - public float ScaledMaxLineGap { get; } - - public float ScaledMaxLeftSideBearing { get; } + public TextBox(IReadOnlyList textLines) => this.TextLines = textLines; - public ReadOnlyCollection TextLines { get; } + public IReadOnlyList TextLines { get; } public TextDirection TextDirection() => this.TextLines[0][0].TextDirection; } @@ -893,22 +892,77 @@ public TextBox( internal sealed class TextLine { private readonly List info = new(); + private float advance = -1; + private float lineHeight = -1; + private float ascender = -1; + private float descender = -1; + private float lineGap = -1; public int Count => this.info.Count; public GlyphLayoutData this[int index] => this.info[index]; public float ScaledLineAdvance() - => this.info.Sum(x => x.ScaledAdvance); + { + if (this.advance > -1) + { + return this.advance; + } + + return this.advance = this.info.Sum(x => x.ScaledAdvance); + } + + public float ScaledMaxLineHeight() + { + if (this.lineHeight > -1) + { + return this.lineHeight; + } + + return this.lineHeight = this.info.Max(x => x.ScaledLineHeight); + } + + public float ScaledMaxAscender() + { + if (this.ascender > -1) + { + return this.ascender; + } + + return this.ascender = this.info.Max(x => x.ScaledAscender); + } + + public float ScaledMaxDescender() + { + if (this.descender > -1) + { + return this.descender; + } + + return this.descender = this.info.Max(x => x.ScaledDescender); + } + + public float ScaledMaxLineGap() + { + if (this.lineGap > -1) + { + return this.lineGap; + } + + return this.lineGap = this.info.Max(x => x.ScaledLineGap); + } public void Add( GlyphMetrics[] metrics, float pointSize, float scaledAdvance, + float scaledLineHeight, + float scaledAscender, + float scaledDescender, BidiRun bidiRun, int graphemeIndex, int offset) - => this.info.Add(new(metrics, pointSize, scaledAdvance, bidiRun, graphemeIndex, offset)); + => this.info.Add(new(metrics, pointSize, scaledAdvance, scaledLineHeight, scaledAscender, scaledDescender, bidiRun, graphemeIndex, offset)); public TextLine SplitAt(LineBreak lineBreak, bool keepAll) { @@ -1125,6 +1179,9 @@ public GlyphLayoutData( GlyphMetrics[] metrics, float pointSize, float scaledAdvance, + float scaledLineHeight, + float scaledAscender, + float scaledDescender, BidiRun bidiRun, int graphemeIndex, int offset) @@ -1132,6 +1189,10 @@ public GlyphLayoutData( this.Metrics = metrics; this.PointSize = pointSize; this.ScaledAdvance = scaledAdvance; + this.ScaledLineHeight = scaledLineHeight; + this.ScaledAscender = scaledAscender; + this.ScaledDescender = scaledDescender; + this.ScaledLineGap = scaledLineHeight - (scaledAscender + scaledDescender); this.BidiRun = bidiRun; this.GraphemeIndex = graphemeIndex; this.Offset = offset; @@ -1145,6 +1206,14 @@ public GlyphLayoutData( public float ScaledAdvance { get; } + public float ScaledLineHeight { get; } + + public float ScaledAscender { get; } + + public float ScaledDescender { get; } + + public float ScaledLineGap { get; } + public BidiRun BidiRun { get; } public TextDirection TextDirection => (TextDirection)this.BidiRun.Direction; diff --git a/src/SixLabors.Fonts/TextMeasurer.cs b/src/SixLabors.Fonts/TextMeasurer.cs index 5701f5c1b..bcc4217d2 100644 --- a/src/SixLabors.Fonts/TextMeasurer.cs +++ b/src/SixLabors.Fonts/TextMeasurer.cs @@ -98,25 +98,28 @@ internal static FontRectangle GetSize(IReadOnlyList glyphLayouts, f float top = int.MaxValue; float bottom = int.MinValue; float right = int.MinValue; + for (int i = 0; i < glyphLayouts.Count; i++) { GlyphLayout glyph = glyphLayouts[i]; Vector2 location = glyph.Location; + float x = location.X; + float y = location.Y - (glyph.Ascender + glyph.LineGap); float lineHeight = glyph.LineHeight; // Avoid trimming zero-width/height marks that extend past the bounds of their base. FontRectangle box = glyph.BoundingBox(1F); - float advanceX = Math.Max(location.X + glyph.Width, box.Right); - float advanceY = Math.Max(location.Y + lineHeight, box.Top + lineHeight); + float advanceX = Math.Max(x + glyph.Width, box.Right); + float advanceY = Math.Max(y + lineHeight, box.Top + box.Height); - if (left > location.X) + if (left > x) { - left = location.X; + left = x; } - if (top > location.Y) + if (top > y) { - top = location.Y; + top = y; } if (right < advanceX) From 41687c7e80bfbf5030dcf4aaf352e35c3a4c82f8 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 11 Mar 2022 15:55:48 +1100 Subject: [PATCH 39/48] Fix initial vertical y offset --- src/SixLabors.Fonts/TextLayout.cs | 35 ++++++++++++++---------- src/SixLabors.Fonts/Unicode/CodePoint.cs | 3 +- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/src/SixLabors.Fonts/TextLayout.cs b/src/SixLabors.Fonts/TextLayout.cs index 908dfbd47..1fc7ba2e6 100644 --- a/src/SixLabors.Fonts/TextLayout.cs +++ b/src/SixLabors.Fonts/TextLayout.cs @@ -217,21 +217,21 @@ private static IReadOnlyList LayoutText(TextBox textBox, TextOption { for (int i = textBox.TextLines.Count - 1; i >= 0; i--) { - glyphs.AddRange(LayoutLineHorizontal(textBox, textBox.TextLines[i], direction, maxScaledAdvance, options, i, ref location, origin)); + glyphs.AddRange(LayoutLineHorizontal(textBox, textBox.TextLines[i], direction, maxScaledAdvance, options, textBox.TextLines.Count - 1 - i, ref location, origin)); } } else if (layoutMode == LayoutMode.VerticalLeftRight) { for (int i = 0; i < textBox.TextLines.Count; i++) { - glyphs.AddRange(LayoutLineVertical(textBox, textBox.TextLines[i], direction, maxScaledAdvance, options, glyphs.Count == 0, ref location)); + glyphs.AddRange(LayoutLineVertical(textBox, textBox.TextLines[i], direction, maxScaledAdvance, options, i, ref location)); } } else { for (int i = textBox.TextLines.Count - 1; i >= 0; i--) { - glyphs.AddRange(LayoutLineVertical(textBox, textBox.TextLines[i], direction, maxScaledAdvance, options, glyphs.Count == 0, ref location)); + glyphs.AddRange(LayoutLineVertical(textBox, textBox.TextLines[i], direction, maxScaledAdvance, options, textBox.TextLines.Count - 1 - i, ref location)); } } @@ -267,7 +267,7 @@ private static IEnumerable LayoutLineHorizontal( location.Y = origin.Y; offsetY = ((scaledMaxAscender + scaledMaxLineGap) * .5F) - (scaledMaxDescender * .5F); - // Vertically centering aligning multiple lines of variable line hight is tricky. + // Vertically centering aligning multiple lines of variable line-hight is tricky. if (textBox.TextLines.Count > 1) { // First negatively offset each line based on its index aligning at the bottom @@ -395,7 +395,7 @@ private static IEnumerable LayoutLineVertical( TextDirection direction, float maxScaledAdvance, TextOptions options, - bool first, + int index, ref Vector2 location) { float originY = location.Y; @@ -404,22 +404,28 @@ private static IEnumerable LayoutLineVertical( // Set the Y-Origin for the line. float scaledMaxLineGap = textLine.ScaledMaxLineGap(); - float scaledMaxAscender = textLine.ScaledMaxAscender() + scaledMaxLineGap; + float scaledMaxAscender = textLine.ScaledMaxAscender(); float scaledMaxDescender = textLine.ScaledMaxDescender(); float scaledMaxLineHeight = textLine.ScaledMaxLineHeight(); + TextLine.GlyphLayoutData first = textLine[0]; + float firstMaxLineGap = first.ScaledLineGap; + float firstMaxAscender = first.ScaledAscender; + float firstMaxDescender = first.ScaledDescender; + float firstMaxLineHeight = first.ScaledLineHeight; + switch (options.VerticalAlignment) { case VerticalAlignment.Top: - offsetY = scaledMaxAscender; + offsetY = firstMaxAscender + firstMaxLineGap; break; case VerticalAlignment.Center: - offsetY = (scaledMaxAscender * .5F) - (scaledMaxDescender * .5F); - offsetY -= (maxScaledAdvance - scaledMaxLineHeight) * .5F; + offsetY = ((firstMaxAscender + firstMaxLineGap) * .5F) - (firstMaxDescender * .5F); + offsetY -= (maxScaledAdvance - firstMaxLineHeight) * .5F; break; case VerticalAlignment.Bottom: - offsetY = -scaledMaxDescender; - offsetY -= maxScaledAdvance - scaledMaxLineHeight; + offsetY = -firstMaxDescender; + offsetY -= maxScaledAdvance - firstMaxLineHeight; break; } @@ -451,7 +457,8 @@ private static IEnumerable LayoutLineVertical( location.Y += offsetY; - if (first) + bool isFirstLine = index == 0; + if (isFirstLine) { // Set the X-Origin for horizontal alignment. switch (options.HorizontalAlignment) @@ -478,10 +485,10 @@ private static IEnumerable LayoutLineVertical( location.X += offsetX; List glyphs = new(); - float xWidth = scaledMaxLineHeight * (first ? 1F : options.LineSpacing); + float xWidth = scaledMaxLineHeight * (isFirstLine ? 1F : options.LineSpacing); float xLineAdvance = scaledMaxLineHeight * options.LineSpacing; - if (first) + if (isFirstLine) { xLineAdvance -= (xLineAdvance - scaledMaxLineHeight) * .5F; } diff --git a/src/SixLabors.Fonts/Unicode/CodePoint.cs b/src/SixLabors.Fonts/Unicode/CodePoint.cs index 01c262e54..5f1c2f3f4 100644 --- a/src/SixLabors.Fonts/Unicode/CodePoint.cs +++ b/src/SixLabors.Fonts/Unicode/CodePoint.cs @@ -263,11 +263,12 @@ public static bool IsZeroWidthJoiner(CodePoint codePoint) /// /// Gets a value indicating whether the given codepoint is a variation selector. + /// /// /// The codepoint to evaluate. /// if is a variation selector character; otherwise, public static bool IsVariationSelector(CodePoint codePoint) - => (codePoint.Value & 0xFFF0) == 0xFE00; // See https://en.wikipedia.org/wiki/Variation_Selectors_%28Unicode_block%29 + => (codePoint.Value & 0xFFF0) == 0xFE00; /// /// Gets a value indicating whether the given codepoint is a control character. From ceb56565ba50c70efd1dc16fc08a268a8597b514 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sat, 12 Mar 2022 01:13:41 +1100 Subject: [PATCH 40/48] Fix vertical layout + linebreak --- src/SixLabors.Fonts/TextLayout.cs | 48 ++++++++++++++++--------------- 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/src/SixLabors.Fonts/TextLayout.cs b/src/SixLabors.Fonts/TextLayout.cs index 1fc7ba2e6..f28438731 100644 --- a/src/SixLabors.Fonts/TextLayout.cs +++ b/src/SixLabors.Fonts/TextLayout.cs @@ -408,24 +408,16 @@ private static IEnumerable LayoutLineVertical( float scaledMaxDescender = textLine.ScaledMaxDescender(); float scaledMaxLineHeight = textLine.ScaledMaxLineHeight(); - TextLine.GlyphLayoutData first = textLine[0]; - float firstMaxLineGap = first.ScaledLineGap; - float firstMaxAscender = first.ScaledAscender; - float firstMaxDescender = first.ScaledDescender; - float firstMaxLineHeight = first.ScaledLineHeight; - switch (options.VerticalAlignment) { case VerticalAlignment.Top: - offsetY = firstMaxAscender + firstMaxLineGap; + offsetY = 0; break; case VerticalAlignment.Center: - offsetY = ((firstMaxAscender + firstMaxLineGap) * .5F) - (firstMaxDescender * .5F); - offsetY -= (maxScaledAdvance - firstMaxLineHeight) * .5F; + offsetY -= maxScaledAdvance * .5F; break; case VerticalAlignment.Bottom: - offsetY = -firstMaxDescender; - offsetY -= maxScaledAdvance - firstMaxLineHeight; + offsetY -= maxScaledAdvance; break; } @@ -464,16 +456,15 @@ private static IEnumerable LayoutLineVertical( switch (options.HorizontalAlignment) { case HorizontalAlignment.Right: - offsetX = -(scaledMaxLineHeight * options.LineSpacing); - for (int i = textBox.TextLines.Count - 1; i > 0; i--) + // The textline methods are memoized so we're safe to call them multiple times. + for (int i = 0; i < textBox.TextLines.Count; i++) { offsetX -= textBox.TextLines[i].ScaledMaxLineHeight() * options.LineSpacing; } break; case HorizontalAlignment.Center: - offsetX = -(scaledMaxLineHeight * options.LineSpacing * .5F); - for (int i = textBox.TextLines.Count - 1; i > 0; i--) + for (int i = 0; i < textBox.TextLines.Count; i++) { offsetX -= textBox.TextLines[i].ScaledMaxLineHeight() * options.LineSpacing * .5F; } @@ -495,19 +486,19 @@ private static IEnumerable LayoutLineVertical( for (int i = 0; i < textLine.Count; i++) { - TextLine.GlyphLayoutData info = textLine[i]; - if (info.IsNewLine) + TextLine.GlyphLayoutData data = textLine[i]; + if (data.IsNewLine) { location.X += xLineAdvance; location.Y = originY; continue; } - foreach (GlyphMetrics metric in info.Metrics) + foreach (GlyphMetrics metric in data.Metrics) { - Vector2 scale = new Vector2(info.PointSize) / metric.ScaleFactor; + Vector2 scale = new Vector2(data.PointSize) / metric.ScaleFactor; float advanceX = xLineAdvance; - float advanceY = info.ScaledAdvance; + float advanceY = data.ScaledAdvance; // Advance Width & Height can be 0 which is fine for layout but not for measuring. if (advanceX == 0) @@ -521,8 +512,8 @@ private static IEnumerable LayoutLineVertical( } glyphs.Add(new GlyphLayout( - new Glyph(metric, info.PointSize), - location + new Vector2((xWidth - (metric.AdvanceWidth * scale.X)) * .5F, 0), + new Glyph(metric, data.PointSize), + location + new Vector2((xWidth - (metric.AdvanceWidth * scale.X)) * .5F, data.ScaledAscender), scaledMaxAscender, scaledMaxDescender, scaledMaxLineGap, @@ -532,7 +523,7 @@ private static IEnumerable LayoutLineVertical( i == 0)); } - location.Y += info.ScaledAdvance; + location.Y += data.ScaledAdvance; } location.Y = originY; @@ -789,6 +780,17 @@ private static TextBox BreakLines( textLine = new(); lineAdvance = 0; } + else if (currentLineBreak.PositionWrap == codePointIndex) + { + // Split the current textline into two at the current wrapping point. + TextLine split = textLine.SplitAt(currentLineBreak, keepAll); + if (split != textLine) + { + textLines.Add(textLine.BidiReOrder()); + textLine = split; + lineAdvance = split.ScaledLineAdvance(); + } + } else if (currentLineBreak.PositionMeasure == codePointIndex) { // Exact length match. Check for CJK From d7b4326982d3cbb0e3a04662e67b392bef443e1f Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sat, 12 Mar 2022 01:44:52 +1100 Subject: [PATCH 41/48] Update test to match fixes --- src/SixLabors.Fonts/TextLayout.cs | 22 +++++++++---------- .../GPos/GPosTableTests.cs | 4 ++-- .../SixLabors.Fonts.Tests/TextLayoutTests.cs | 18 +++++++-------- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/SixLabors.Fonts/TextLayout.cs b/src/SixLabors.Fonts/TextLayout.cs index f28438731..256142c5e 100644 --- a/src/SixLabors.Fonts/TextLayout.cs +++ b/src/SixLabors.Fonts/TextLayout.cs @@ -780,17 +780,6 @@ private static TextBox BreakLines( textLine = new(); lineAdvance = 0; } - else if (currentLineBreak.PositionWrap == codePointIndex) - { - // Split the current textline into two at the current wrapping point. - TextLine split = textLine.SplitAt(currentLineBreak, keepAll); - if (split != textLine) - { - textLines.Add(textLine.BidiReOrder()); - textLine = split; - lineAdvance = split.ScaledLineAdvance(); - } - } else if (currentLineBreak.PositionMeasure == codePointIndex) { // Exact length match. Check for CJK @@ -812,6 +801,17 @@ private static TextBox BreakLines( lineAdvance = 0; } } + else if (currentLineBreak.PositionWrap == codePointIndex) + { + // Split the current textline into two at the current wrapping point. + TextLine split = textLine.SplitAt(currentLineBreak, keepAll); + if (split != textLine) + { + textLines.Add(textLine.BidiReOrder()); + textLine = split; + lineAdvance = split.ScaledLineAdvance(); + } + } else if (lastLineBreak.PositionWrap < codePointIndex) { // Split the current textline into two at the last wrapping point. diff --git a/tests/SixLabors.Fonts.Tests/Tables/AdvancedTypographic/GPos/GPosTableTests.cs b/tests/SixLabors.Fonts.Tests/Tables/AdvancedTypographic/GPos/GPosTableTests.cs index e9147f6a3..a17d99bf3 100644 --- a/tests/SixLabors.Fonts.Tests/Tables/AdvancedTypographic/GPos/GPosTableTests.cs +++ b/tests/SixLabors.Fonts.Tests/Tables/AdvancedTypographic/GPos/GPosTableTests.cs @@ -492,8 +492,8 @@ public void MarkAnchoring_Works() int[] expectedGlyphIndices = { 759, 989 }; FontRectangle[] expectedFontRectangles = { - new(246, 263.999878f, 363, 322), - new(71, 324.999878f, 966, 1573), + new(246, 264, 363, 322), + new(71, 325, 966, 1573), }; // act diff --git a/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs b/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs index 97c9be22e..a4f2d4637 100644 --- a/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs +++ b/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs @@ -56,17 +56,17 @@ public void FakeFontGetGlyph() [InlineData( VerticalAlignment.Center, HorizontalAlignment.Left, - -30, + -35, 10)] [InlineData( VerticalAlignment.Center, HorizontalAlignment.Right, - -30, + -35, -320)] [InlineData( VerticalAlignment.Center, HorizontalAlignment.Center, - -30, + -35, -155)] public void VerticalAlignmentTests( VerticalAlignment vertical, @@ -251,12 +251,12 @@ public void MeasureTextWordWrappingVerticalRightLeft(string text, float height, #if OS_WINDOWS [Theory] - [InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious Taumatawhakatangihangakoauauotamateaturipukakapikimaungahoronukupokaiwhenuakitanatahu グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalTopBottom, WordBreaking.Normal, 134, 871)] - [InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious Taumatawhakatangihangakoauauotamateaturipukakapikimaungahoronukupokaiwhenuakitanatahu グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalTopBottom, WordBreaking.BreakAll, 160, 400)] - [InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalTopBottom, WordBreaking.KeepAll, 80, 700)] - [InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious Taumatawhakatangihangakoauauotamateaturipukakapikimaungahoronukupokaiwhenuakitanatahu グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalBottomTop, WordBreaking.Normal, 134, 871)] - [InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious Taumatawhakatangihangakoauauotamateaturipukakapikimaungahoronukupokaiwhenuakitanatahu グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalBottomTop, WordBreaking.BreakAll, 160, 400)] - [InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalBottomTop, WordBreaking.KeepAll, 80, 700)] + [InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious Taumatawhakatangihangakoauauotamateaturipukakapikimaungahoronukupokaiwhenuakitanatahu グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalTopBottom, WordBreaking.Normal, 123, 871)] + [InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious Taumatawhakatangihangakoauauotamateaturipukakapikimaungahoronukupokaiwhenuakitanatahu グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalTopBottom, WordBreaking.BreakAll, 150, 400)] + [InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalTopBottom, WordBreaking.KeepAll, 73, 700)] + [InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious Taumatawhakatangihangakoauauotamateaturipukakapikimaungahoronukupokaiwhenuakitanatahu グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalBottomTop, WordBreaking.Normal, 123, 871)] + [InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious Taumatawhakatangihangakoauauotamateaturipukakapikimaungahoronukupokaiwhenuakitanatahu グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalBottomTop, WordBreaking.BreakAll, 150, 400)] + [InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalBottomTop, WordBreaking.KeepAll, 73, 700)] public void MeasureTextWordBreak(string text, LayoutMode layoutMode, WordBreaking wordBreaking, float height, float width) { // Testing using Windows only to ensure that actual glyphs are rendered From db51f985cded663e4b99394b7e16813dd04c032b Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sun, 13 Mar 2022 20:55:01 +1100 Subject: [PATCH 42/48] Drop precision for .NET FX --- .../Tables/AdvancedTypographic/GPos/GPosTableTests.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/SixLabors.Fonts.Tests/Tables/AdvancedTypographic/GPos/GPosTableTests.cs b/tests/SixLabors.Fonts.Tests/Tables/AdvancedTypographic/GPos/GPosTableTests.cs index a17d99bf3..cc0ce26bc 100644 --- a/tests/SixLabors.Fonts.Tests/Tables/AdvancedTypographic/GPos/GPosTableTests.cs +++ b/tests/SixLabors.Fonts.Tests/Tables/AdvancedTypographic/GPos/GPosTableTests.cs @@ -509,7 +509,8 @@ public void MarkAnchoring_Works() for (int i = 0; i < expectedFontRectangles.Length; i++) { - CompareRectangle(expectedFontRectangles[i], renderer.GlyphRects[i]); + // NET FX requires lower precision + CompareRectangle(expectedFontRectangles[i], renderer.GlyphRects[i], 3); } } From 6a883afa90e62b6c71d7a07de08f44a559360280 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 14 Mar 2022 22:28:19 +1100 Subject: [PATCH 43/48] Fix line height --- src/SixLabors.Fonts/TextLayout.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/SixLabors.Fonts/TextLayout.cs b/src/SixLabors.Fonts/TextLayout.cs index 256142c5e..5f09983c6 100644 --- a/src/SixLabors.Fonts/TextLayout.cs +++ b/src/SixLabors.Fonts/TextLayout.cs @@ -251,8 +251,8 @@ private static IEnumerable LayoutLineHorizontal( float scaledMaxLineGap = textLine.ScaledMaxLineGap(); float scaledMaxAscender = textLine.ScaledMaxAscender(); float scaledMaxDescender = textLine.ScaledMaxDescender(); - float scaledMaxLineHeight = textLine.ScaledMaxLineHeight(); - float scaledLineAdvance = scaledMaxDescender * options.LineSpacing; + float scaledMaxLineHeight = textLine.ScaledMaxLineHeight() * options.LineSpacing; + float scaledLineAdvance = scaledMaxLineHeight - (scaledMaxAscender + scaledMaxLineGap); float originX = location.X; float offsetY = 0; float offsetX = 0; @@ -274,7 +274,7 @@ private static IEnumerable LayoutLineHorizontal( // as we do with the bottom alignment below. for (int i = index; i < textBox.TextLines.Count; i++) { - offsetY -= textBox.TextLines[i].ScaledMaxLineHeight(); + offsetY -= textBox.TextLines[i].ScaledMaxLineHeight() * options.LineSpacing; } // Now push all the lines back down by half. @@ -282,7 +282,7 @@ private static IEnumerable LayoutLineHorizontal( for (int i = 0; i < textBox.TextLines.Count; i++) { TextLine current = textBox.TextLines[i]; - offsetY += (current.ScaledMaxLineHeight() + (current.ScaledMaxDescender() * options.LineSpacing)) * .5F; + offsetY += ((current.ScaledMaxLineHeight() * options.LineSpacing) + current.ScaledMaxDescender()) * .5F; } } @@ -292,7 +292,7 @@ private static IEnumerable LayoutLineHorizontal( offsetY = -scaledMaxDescender; for (int i = index; i < textBox.TextLines.Count - 1; i++) { - offsetY -= textBox.TextLines[i].ScaledMaxLineHeight(); + offsetY -= textBox.TextLines[i].ScaledMaxLineHeight() * options.LineSpacing; } break; @@ -803,7 +803,7 @@ private static TextBox BreakLines( } else if (currentLineBreak.PositionWrap == codePointIndex) { - // Split the current textline into two at the current wrapping point. + // Exact length match. Check for CJK TextLine split = textLine.SplitAt(currentLineBreak, keepAll); if (split != textLine) { From a43a592184e076cfb77ef74a3fb610d37734ded2 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 15 Mar 2022 01:15:01 +1100 Subject: [PATCH 44/48] layout box is a fools errand. Leading FTW --- src/SixLabors.Fonts/TextLayout.cs | 59 +++++++------------ .../SixLabors.Fonts.Tests/TextLayoutTests.cs | 18 +++--- 2 files changed, 31 insertions(+), 46 deletions(-) diff --git a/src/SixLabors.Fonts/TextLayout.cs b/src/SixLabors.Fonts/TextLayout.cs index 5f09983c6..5343a180e 100644 --- a/src/SixLabors.Fonts/TextLayout.cs +++ b/src/SixLabors.Fonts/TextLayout.cs @@ -210,14 +210,14 @@ private static IReadOnlyList LayoutText(TextBox textBox, TextOption { for (int i = 0; i < textBox.TextLines.Count; i++) { - glyphs.AddRange(LayoutLineHorizontal(textBox, textBox.TextLines[i], direction, maxScaledAdvance, options, i, ref location, origin)); + glyphs.AddRange(LayoutLineHorizontal(textBox, textBox.TextLines[i], direction, maxScaledAdvance, options, i, ref location)); } } else if (layoutMode == LayoutMode.HorizontalBottomTop) { for (int i = textBox.TextLines.Count - 1; i >= 0; i--) { - glyphs.AddRange(LayoutLineHorizontal(textBox, textBox.TextLines[i], direction, maxScaledAdvance, options, textBox.TextLines.Count - 1 - i, ref location, origin)); + glyphs.AddRange(LayoutLineHorizontal(textBox, textBox.TextLines[i], direction, maxScaledAdvance, options, textBox.TextLines.Count - 1 - i, ref location)); } } else if (layoutMode == LayoutMode.VerticalLeftRight) @@ -245,61 +245,46 @@ private static IEnumerable LayoutLineHorizontal( float maxScaledAdvance, TextOptions options, int index, - ref Vector2 location, - Vector2 origin) + ref Vector2 location) { float scaledMaxLineGap = textLine.ScaledMaxLineGap(); float scaledMaxAscender = textLine.ScaledMaxAscender(); float scaledMaxDescender = textLine.ScaledMaxDescender(); float scaledMaxLineHeight = textLine.ScaledMaxLineHeight() * options.LineSpacing; - float scaledLineAdvance = scaledMaxLineHeight - (scaledMaxAscender + scaledMaxLineGap); + float scaledLineAdvance = scaledMaxLineHeight; float originX = location.X; float offsetY = 0; float offsetX = 0; // Set the Y-Origin for the line. - switch (options.VerticalAlignment) + if (index == 0) { - case VerticalAlignment.Top: - offsetY = scaledMaxAscender + scaledMaxLineGap; - break; - case VerticalAlignment.Center: - location.Y = origin.Y; - offsetY = ((scaledMaxAscender + scaledMaxLineGap) * .5F) - (scaledMaxDescender * .5F); - - // Vertically centering aligning multiple lines of variable line-hight is tricky. - if (textBox.TextLines.Count > 1) - { - // First negatively offset each line based on its index aligning at the bottom - // as we do with the bottom alignment below. - for (int i = index; i < textBox.TextLines.Count; i++) + switch (options.VerticalAlignment) + { + case VerticalAlignment.Top: + offsetY = scaledMaxAscender + scaledMaxLineGap; + break; + case VerticalAlignment.Center: + offsetY = (scaledMaxAscender + scaledMaxLineGap - scaledMaxDescender) * .5F; + for (int i = index; i < textBox.TextLines.Count - 1; i++) { - offsetY -= textBox.TextLines[i].ScaledMaxLineHeight() * options.LineSpacing; + offsetY -= textBox.TextLines[i].ScaledMaxLineHeight() * options.LineSpacing * .5F; } - // Now push all the lines back down by half. - // The textline methods are memoized so we're safe to call them multiple times. - for (int i = 0; i < textBox.TextLines.Count; i++) + break; + case VerticalAlignment.Bottom: + offsetY = -scaledMaxDescender; + for (int i = index; i < textBox.TextLines.Count - 1; i++) { - TextLine current = textBox.TextLines[i]; - offsetY += ((current.ScaledMaxLineHeight() * options.LineSpacing) + current.ScaledMaxDescender()) * .5F; + offsetY -= textBox.TextLines[i].ScaledMaxLineHeight() * options.LineSpacing; } - } - break; - case VerticalAlignment.Bottom: - location.Y = origin.Y; - offsetY = -scaledMaxDescender; - for (int i = index; i < textBox.TextLines.Count - 1; i++) - { - offsetY -= textBox.TextLines[i].ScaledMaxLineHeight() * options.LineSpacing; - } + break; + } - break; + location.Y += offsetY; } - location.Y += offsetY; - // Set the X-Origin for horizontal alignment. switch (options.HorizontalAlignment) { diff --git a/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs b/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs index a4f2d4637..291fd814a 100644 --- a/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs +++ b/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs @@ -56,17 +56,17 @@ public void FakeFontGetGlyph() [InlineData( VerticalAlignment.Center, HorizontalAlignment.Left, - -35, + -30, 10)] [InlineData( VerticalAlignment.Center, HorizontalAlignment.Right, - -35, + -30, -320)] [InlineData( VerticalAlignment.Center, HorizontalAlignment.Center, - -35, + -30, -155)] public void VerticalAlignmentTests( VerticalAlignment vertical, @@ -251,12 +251,12 @@ public void MeasureTextWordWrappingVerticalRightLeft(string text, float height, #if OS_WINDOWS [Theory] - [InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious Taumatawhakatangihangakoauauotamateaturipukakapikimaungahoronukupokaiwhenuakitanatahu グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalTopBottom, WordBreaking.Normal, 123, 871)] - [InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious Taumatawhakatangihangakoauauotamateaturipukakapikimaungahoronukupokaiwhenuakitanatahu グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalTopBottom, WordBreaking.BreakAll, 150, 400)] - [InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalTopBottom, WordBreaking.KeepAll, 73, 700)] - [InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious Taumatawhakatangihangakoauauotamateaturipukakapikimaungahoronukupokaiwhenuakitanatahu グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalBottomTop, WordBreaking.Normal, 123, 871)] - [InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious Taumatawhakatangihangakoauauotamateaturipukakapikimaungahoronukupokaiwhenuakitanatahu グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalBottomTop, WordBreaking.BreakAll, 150, 400)] - [InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalBottomTop, WordBreaking.KeepAll, 73, 700)] + [InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious Taumatawhakatangihangakoauauotamateaturipukakapikimaungahoronukupokaiwhenuakitanatahu グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalTopBottom, WordBreaking.Normal, 125, 871)] + [InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious Taumatawhakatangihangakoauauotamateaturipukakapikimaungahoronukupokaiwhenuakitanatahu グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalTopBottom, WordBreaking.BreakAll, 152, 400)] + [InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalTopBottom, WordBreaking.KeepAll, 76, 700)] + [InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious Taumatawhakatangihangakoauauotamateaturipukakapikimaungahoronukupokaiwhenuakitanatahu グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalBottomTop, WordBreaking.Normal, 125, 871)] + [InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious Taumatawhakatangihangakoauauotamateaturipukakapikimaungahoronukupokaiwhenuakitanatahu グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalBottomTop, WordBreaking.BreakAll, 152, 400)] + [InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalBottomTop, WordBreaking.KeepAll, 76, 700)] public void MeasureTextWordBreak(string text, LayoutMode layoutMode, WordBreaking wordBreaking, float height, float width) { // Testing using Windows only to ensure that actual glyphs are rendered From 39773e680338b229cc285b52a3ba97ef9bf63182 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 21 Mar 2022 02:12:03 +1100 Subject: [PATCH 45/48] Use graphemes, fix line-gap usage, fix mixed line-height --- src/SixLabors.Fonts/TextLayout.cs | 282 ++++++++++-------- src/SixLabors.Fonts/TextMeasurer.cs | 2 +- src/SixLabors.Fonts/TextRun.cs | 33 +- .../GPos/GPosTableTests.cs | 76 ++--- .../SixLabors.Fonts.Tests/TextLayoutTests.cs | 12 +- 5 files changed, 227 insertions(+), 178 deletions(-) diff --git a/src/SixLabors.Fonts/TextLayout.cs b/src/SixLabors.Fonts/TextLayout.cs index 5343a180e..bd8a12dc9 100644 --- a/src/SixLabors.Fonts/TextLayout.cs +++ b/src/SixLabors.Fonts/TextLayout.cs @@ -201,11 +201,10 @@ private static IReadOnlyList LayoutText(TextBox textBox, TextOption { LayoutMode layoutMode = options.LayoutMode; List glyphs = new(); - - Vector2 origin = options.Origin / options.Dpi; - Vector2 location = origin; - float maxScaledAdvance = textBox.TextLines.Max(x => x.ScaledLineAdvance()); + Vector2 location = options.Origin / options.Dpi; + float maxScaledAdvance = textBox.ScaledMaxAdvance(); TextDirection direction = textBox.TextDirection(); + if (layoutMode == LayoutMode.HorizontalTopBottom) { for (int i = 0; i < textBox.TextLines.Count; i++) @@ -215,9 +214,10 @@ private static IReadOnlyList LayoutText(TextBox textBox, TextOption } else if (layoutMode == LayoutMode.HorizontalBottomTop) { + int index = 0; for (int i = textBox.TextLines.Count - 1; i >= 0; i--) { - glyphs.AddRange(LayoutLineHorizontal(textBox, textBox.TextLines[i], direction, maxScaledAdvance, options, textBox.TextLines.Count - 1 - i, ref location)); + glyphs.AddRange(LayoutLineHorizontal(textBox, textBox.TextLines[i], direction, maxScaledAdvance, options, index++, ref location)); } } else if (layoutMode == LayoutMode.VerticalLeftRight) @@ -229,9 +229,10 @@ private static IReadOnlyList LayoutText(TextBox textBox, TextOption } else { + int index = 0; for (int i = textBox.TextLines.Count - 1; i >= 0; i--) { - glyphs.AddRange(LayoutLineVertical(textBox, textBox.TextLines[i], direction, maxScaledAdvance, options, textBox.TextLines.Count - 1 - i, ref location)); + glyphs.AddRange(LayoutLineVertical(textBox, textBox.TextLines[i], direction, maxScaledAdvance, options, index++, ref location)); } } @@ -247,36 +248,66 @@ private static IEnumerable LayoutLineHorizontal( int index, ref Vector2 location) { - float scaledMaxLineGap = textLine.ScaledMaxLineGap(); - float scaledMaxAscender = textLine.ScaledMaxAscender(); - float scaledMaxDescender = textLine.ScaledMaxDescender(); - float scaledMaxLineHeight = textLine.ScaledMaxLineHeight() * options.LineSpacing; - float scaledLineAdvance = scaledMaxLineHeight; + float scaledMaxLineGap = textBox.ScaledMaxLineGap(textLine.MaxPointSize); + float scaledMaxAscender = textBox.ScaledMaxAscender(textLine.MaxPointSize); + float scaledMaxDescender = textBox.ScaledMaxDescender(textLine.MaxPointSize); + float scaledMaxLineHeight = textBox.ScaledMaxLineHeight(textLine.MaxPointSize); + + bool isFirstLine = index == 0; + bool isLastLine = index == textBox.TextLines.Count - 1; + float scaledLineAdvance = scaledMaxLineHeight * options.LineSpacing; + + // Recalculate the advance based upon the next line. + // If larger, we want to scale it up to ensure it it pushed down far enough. + // We split the different exactly to line up the text at the exact vertical center. + if (!isFirstLine && !isLastLine) + { + TextLine next = textBox.TextLines[index + 1]; + float nextLineAdvance = textBox.ScaledMaxLineHeight(next.MaxPointSize) * options.LineSpacing; + scaledLineAdvance += (nextLineAdvance - scaledLineAdvance) * .5F; + } + float originX = location.X; float offsetY = 0; float offsetX = 0; // Set the Y-Origin for the line. - if (index == 0) + if (isFirstLine) { switch (options.VerticalAlignment) { case VerticalAlignment.Top: - offsetY = scaledMaxAscender + scaledMaxLineGap; + offsetY = scaledMaxAscender; break; case VerticalAlignment.Center: - offsetY = (scaledMaxAscender + scaledMaxLineGap - scaledMaxDescender) * .5F; + offsetY = (scaledMaxAscender - (scaledMaxDescender + scaledMaxLineGap)) * .5F; for (int i = index; i < textBox.TextLines.Count - 1; i++) { - offsetY -= textBox.TextLines[i].ScaledMaxLineHeight() * options.LineSpacing * .5F; + float advance = textBox.ScaledMaxLineHeight(textBox.TextLines[i].MaxPointSize); + if (i != 0) + { + TextLine next = textBox.TextLines[index + 1]; + float nextLineAdvance = textBox.ScaledMaxLineHeight(next.MaxPointSize); + advance += (nextLineAdvance - advance) * .5F; + } + + offsetY -= advance * options.LineSpacing * .5F; } break; case VerticalAlignment.Bottom: - offsetY = -scaledMaxDescender; + offsetY = -(scaledMaxDescender + scaledMaxLineGap); for (int i = index; i < textBox.TextLines.Count - 1; i++) { - offsetY -= textBox.TextLines[i].ScaledMaxLineHeight() * options.LineSpacing; + float advance = textBox.ScaledMaxLineHeight(textBox.TextLines[i].MaxPointSize); + if (i != 0) + { + TextLine next = textBox.TextLines[index + 1]; + float nextLineAdvance = textBox.ScaledMaxLineHeight(next.MaxPointSize); + advance += (nextLineAdvance - advance) * .5F; + } + + offsetY -= advance * options.LineSpacing; } break; @@ -302,10 +333,10 @@ private static IEnumerable LayoutLineHorizontal( switch (options.TextAlignment) { case TextAlignment.End: - offsetX += maxScaledAdvance - textLine.ScaledLineAdvance(); + offsetX += maxScaledAdvance - textLine.ScaledLineAdvance; break; case TextAlignment.Center: - offsetX += (maxScaledAdvance * .5F) - (textLine.ScaledLineAdvance() * .5F); + offsetX += (maxScaledAdvance * .5F) - (textLine.ScaledLineAdvance * .5F); break; } } @@ -314,10 +345,10 @@ private static IEnumerable LayoutLineHorizontal( switch (options.TextAlignment) { case TextAlignment.Start: - offsetX += maxScaledAdvance - textLine.ScaledLineAdvance(); + offsetX += maxScaledAdvance - textLine.ScaledLineAdvance; break; case TextAlignment.Center: - offsetX += (maxScaledAdvance * .5F) - (textLine.ScaledLineAdvance() * .5F); + offsetX += (maxScaledAdvance * .5F) - (textLine.ScaledLineAdvance * .5F); break; } } @@ -327,18 +358,18 @@ private static IEnumerable LayoutLineHorizontal( List glyphs = new(); for (int i = 0; i < textLine.Count; i++) { - TextLine.GlyphLayoutData info = textLine[i]; - if (info.IsNewLine) + TextLine.GlyphLayoutData data = textLine[i]; + if (data.IsNewLine) { location.Y += scaledLineAdvance; continue; } - foreach (GlyphMetrics metric in info.Metrics) + foreach (GlyphMetrics metric in data.Metrics) { // Advance Width & Height can be 0 which is fine for layout but not for measuring. - Vector2 scale = new Vector2(info.PointSize) / metric.ScaleFactor; - float advanceX = info.ScaledAdvance; + Vector2 scale = new Vector2(data.PointSize) / metric.ScaleFactor; + float advanceX = data.ScaledAdvance; float advanceY = metric.AdvanceHeight * scale.Y; if (advanceX == 0) { @@ -351,18 +382,18 @@ private static IEnumerable LayoutLineHorizontal( } glyphs.Add(new GlyphLayout( - new Glyph(metric, info.PointSize), + new Glyph(metric, data.PointSize), location, scaledMaxAscender, scaledMaxDescender, scaledMaxLineGap, - scaledMaxLineHeight, + scaledLineAdvance, advanceX, advanceY, i == 0)); } - location.X += info.ScaledAdvance; + location.X += data.ScaledAdvance; } location.X = originX; @@ -388,10 +419,10 @@ private static IEnumerable LayoutLineVertical( float offsetX = 0; // Set the Y-Origin for the line. - float scaledMaxLineGap = textLine.ScaledMaxLineGap(); - float scaledMaxAscender = textLine.ScaledMaxAscender(); - float scaledMaxDescender = textLine.ScaledMaxDescender(); - float scaledMaxLineHeight = textLine.ScaledMaxLineHeight(); + float scaledMaxLineGap = textBox.ScaledMaxLineGap(textLine.MaxPointSize); + float scaledMaxAscender = textBox.ScaledMaxAscender(textLine.MaxPointSize); + float scaledMaxDescender = textBox.ScaledMaxDescender(textLine.MaxPointSize); + float scaledMaxLineHeight = textBox.ScaledMaxLineHeight(textLine.MaxPointSize); switch (options.VerticalAlignment) { @@ -412,10 +443,10 @@ private static IEnumerable LayoutLineVertical( switch (options.TextAlignment) { case TextAlignment.End: - offsetY += maxScaledAdvance - textLine.ScaledLineAdvance(); + offsetY += maxScaledAdvance - textLine.ScaledLineAdvance; break; case TextAlignment.Center: - offsetY += (maxScaledAdvance * .5F) - (textLine.ScaledLineAdvance() * .5F); + offsetY += (maxScaledAdvance * .5F) - (textLine.ScaledLineAdvance * .5F); break; } } @@ -424,10 +455,10 @@ private static IEnumerable LayoutLineVertical( switch (options.TextAlignment) { case TextAlignment.Start: - offsetY += maxScaledAdvance - textLine.ScaledLineAdvance(); + offsetY += maxScaledAdvance - textLine.ScaledLineAdvance; break; case TextAlignment.Center: - offsetY += (maxScaledAdvance * .5F) - (textLine.ScaledLineAdvance() * .5F); + offsetY += (maxScaledAdvance * .5F) - (textLine.ScaledLineAdvance * .5F); break; } } @@ -444,14 +475,14 @@ private static IEnumerable LayoutLineVertical( // The textline methods are memoized so we're safe to call them multiple times. for (int i = 0; i < textBox.TextLines.Count; i++) { - offsetX -= textBox.TextLines[i].ScaledMaxLineHeight() * options.LineSpacing; + offsetX -= textBox.ScaledMaxLineHeight(textBox.TextLines[i].MaxPointSize) * options.LineSpacing; } break; case HorizontalAlignment.Center: for (int i = 0; i < textBox.TextLines.Count; i++) { - offsetX -= textBox.TextLines[i].ScaledMaxLineHeight() * options.LineSpacing * .5F; + offsetX -= textBox.ScaledMaxLineHeight(textBox.TextLines[i].MaxPointSize) * options.LineSpacing * .5F; } break; @@ -547,6 +578,11 @@ private static bool DoFontRun( int graphemeCodePointIndex = 0; int charIndex = 0; + if (graphemeIndex == textRuns[textRun].End) + { + textRun++; + } + // Now enumerate through each codepoint in the grapheme. bool skipNextCodePoint = false; var codePointEnumerator = new SpanCodePointEnumerator(graphemeEnumerator.Current); @@ -557,11 +593,6 @@ private static bool DoFontRun( bidiRun++; } - if (codePointIndex == textRuns[textRun].End) - { - textRun++; - } - if (skipNextCodePoint) { codePointIndex++; @@ -775,7 +806,7 @@ private static TextBox BreakLines( { textLines.Add(textLine.BidiReOrder()); textLine = split; - lineAdvance = split.ScaledLineAdvance(); + lineAdvance = split.ScaledLineAdvance; } } else @@ -794,7 +825,7 @@ private static TextBox BreakLines( { textLines.Add(textLine.BidiReOrder()); textLine = split; - lineAdvance = split.ScaledLineAdvance(); + lineAdvance = split.ScaledLineAdvance; } } else if (lastLineBreak.PositionWrap < codePointIndex) @@ -805,7 +836,7 @@ private static TextBox BreakLines( { textLines.Add(textLine.BidiReOrder()); textLine = split; - lineAdvance = split.ScaledLineAdvance(); + lineAdvance = split.ScaledLineAdvance; } } } @@ -844,8 +875,15 @@ private static TextBox BreakLines( GlyphMetrics metric = metrics[0]; float scaleY = pointSize / metric.ScaleFactor.Y; float ascender = metric.FontMetrics.Ascender * scaleY; + if (metric.TopSideBearing < 0) + { + // Adjust for glyphs with a negative tsb. e.g. emoji. + ascender -= metric.TopSideBearing * scaleY; + } + float descender = Math.Abs(metric.FontMetrics.Descender * scaleY); float lineHeight = metric.FontMetrics.LineHeight * scaleY; + float lineGap = lineHeight - (ascender + descender); // Add our metrics to the line. lineAdvance += glyphAdvance; @@ -856,6 +894,7 @@ private static TextBox BreakLines( lineHeight, ascender, descender, + lineGap, bidiRuns[bidiMap[codePointIndex]], graphemeIndex, codePointIndex); @@ -880,71 +919,44 @@ internal sealed class TextBox public IReadOnlyList TextLines { get; } - public TextDirection TextDirection() => this.TextLines[0][0].TextDirection; - } + // TODO: It would be very good to cache these. + public float ScaledMaxAdvance() + => this.TextLines.Max(x => x.ScaledLineAdvance); - internal sealed class TextLine - { - private readonly List info = new(); - private float advance = -1; - private float lineHeight = -1; - private float ascender = -1; - private float descender = -1; - private float lineGap = -1; + public float ScaledMaxLineHeight(float pointSize) + => this.TextLines.Where(x => x.MaxPointSize == pointSize).Max(x => x.ScaledMaxLineHeight); - public int Count => this.info.Count; + public float ScaledMaxAscender(float pointSize) + => this.TextLines.Where(x => x.MaxPointSize == pointSize).Max(x => x.ScaledMaxAscender); - public GlyphLayoutData this[int index] => this.info[index]; + public float ScaledMaxDescender(float pointSize) + => this.TextLines.Where(x => x.MaxPointSize == pointSize).Max(x => x.ScaledMaxDescender); - public float ScaledLineAdvance() - { - if (this.advance > -1) - { - return this.advance; - } + public float ScaledMaxLineGap(float pointSize) + => this.TextLines.Where(x => x.MaxPointSize == pointSize).Max(x => x.ScaledMaxLineGap); - return this.advance = this.info.Sum(x => x.ScaledAdvance); - } + public TextDirection TextDirection() => this.TextLines[0][0].TextDirection; + } - public float ScaledMaxLineHeight() - { - if (this.lineHeight > -1) - { - return this.lineHeight; - } + internal sealed class TextLine + { + private readonly List data = new(); - return this.lineHeight = this.info.Max(x => x.ScaledLineHeight); - } + public int Count => this.data.Count; - public float ScaledMaxAscender() - { - if (this.ascender > -1) - { - return this.ascender; - } + public float MaxPointSize { get; private set; } = -1; - return this.ascender = this.info.Max(x => x.ScaledAscender); - } + public float ScaledLineAdvance { get; private set; } = 0; - public float ScaledMaxDescender() - { - if (this.descender > -1) - { - return this.descender; - } + public float ScaledMaxLineHeight { get; private set; } = -1; - return this.descender = this.info.Max(x => x.ScaledDescender); - } + public float ScaledMaxAscender { get; private set; } = -1; - public float ScaledMaxLineGap() - { - if (this.lineGap > -1) - { - return this.lineGap; - } + public float ScaledMaxDescender { get; private set; } = -1; - return this.lineGap = this.info.Max(x => x.ScaledLineGap); - } + public float ScaledMaxLineGap { get; private set; } = -1; + + public GlyphLayoutData this[int index] => this.data[index]; public void Add( GlyphMetrics[] metrics, @@ -953,18 +965,32 @@ public void Add( float scaledLineHeight, float scaledAscender, float scaledDescender, + float scaledLineGap, BidiRun bidiRun, int graphemeIndex, int offset) - => this.info.Add(new(metrics, pointSize, scaledAdvance, scaledLineHeight, scaledAscender, scaledDescender, bidiRun, graphemeIndex, offset)); + { + // Reset metrics. + // We track the maximum metrics for each line to ensure glyphs can be aligned. + // These will be grouped by the point size for each run within the text to ensure + // multi-line text maintains an even layout for equal point sizes. + this.MaxPointSize = MathF.Max(this.MaxPointSize, pointSize); + this.ScaledLineAdvance += scaledAdvance; + this.ScaledMaxLineHeight = MathF.Max(this.ScaledMaxLineHeight, scaledLineHeight); + this.ScaledMaxAscender = MathF.Max(this.ScaledMaxAscender, scaledAscender); + this.ScaledMaxDescender = MathF.Max(this.ScaledMaxDescender, scaledDescender); + this.ScaledMaxLineGap = MathF.Max(this.ScaledMaxLineGap, scaledLineGap); + + this.data.Add(new(metrics, pointSize, scaledAdvance, scaledLineHeight, scaledAscender, scaledDescender, scaledLineGap, bidiRun, graphemeIndex, offset)); + } public TextLine SplitAt(LineBreak lineBreak, bool keepAll) { - int index = this.info.Count; + int index = this.data.Count; GlyphLayoutData glyphWrap = default; while (index > 0) { - glyphWrap = this.info[--index]; + glyphWrap = this.data[--index]; if (glyphWrap.Offset == lineBreak.PositionWrap) { @@ -985,7 +1011,7 @@ public TextLine SplitAt(LineBreak lineBreak, bool keepAll) // a non CJK codepoint we can break at. while (index > 0) { - glyphWrap = this.info[--index]; + glyphWrap = this.data[--index]; if (!UnicodeUtility.IsCJKCodePoint((uint)glyphWrap.CodePoint.Value)) { index++; @@ -999,15 +1025,24 @@ public TextLine SplitAt(LineBreak lineBreak, bool keepAll) } } + // Create a new line ensuring we capture the intitial metrics. TextLine result = new(); - result.info.AddRange(this.info.GetRange(index, this.info.Count - index)); - this.info.RemoveRange(index, this.info.Count - index); - - // Trim trailing whitespace from previous line. - index = this.info.Count; + result.data.AddRange(this.data.GetRange(index, this.data.Count - index)); + result.ScaledLineAdvance = result.data.Sum(x => x.ScaledAdvance); + result.MaxPointSize = result.data.Max(x => x.PointSize); + result.ScaledMaxAscender = result.data.Max(x => x.ScaledAscender); + result.ScaledMaxDescender = result.data.Max(x => x.ScaledDescender); + result.ScaledMaxLineHeight = result.data.Max(x => x.ScaledLineHeight); + result.ScaledMaxLineGap = result.data.Max(x => x.ScaledLineGap); + + // Remove those items from this line. + this.data.RemoveRange(index, this.data.Count - index); + + // Now trim trailing whitespace from this line. + index = this.data.Count; while (index > 0) { - if (!CodePoint.IsWhiteSpace(this.info[index - 1].CodePoint)) + if (!CodePoint.IsWhiteSpace(this.data[index - 1].CodePoint)) { break; } @@ -1015,23 +1050,31 @@ public TextLine SplitAt(LineBreak lineBreak, bool keepAll) index--; } - if (index < this.info.Count) + if (index < this.data.Count) { - this.info.RemoveRange(index, this.info.Count - index); + this.data.RemoveRange(index, this.data.Count - index); } + // Lastly recalculate this line metrics. + this.ScaledLineAdvance = this.data.Sum(x => x.ScaledAdvance); + this.MaxPointSize = this.data.Max(x => x.PointSize); + this.ScaledMaxAscender = this.data.Max(x => x.ScaledAscender); + this.ScaledMaxDescender = this.data.Max(x => x.ScaledDescender); + this.ScaledMaxLineHeight = this.data.Max(x => x.ScaledLineHeight); + this.ScaledMaxLineGap = this.data.Max(x => x.ScaledLineGap); + return result; } public TextLine BidiReOrder() { // Build up the collection of ordered runs. - BidiRun run = this.info[0].BidiRun; + BidiRun run = this.data[0].BidiRun; OrderedBidiRun orderedRun = new(run.Level); OrderedBidiRun? current = orderedRun; - for (int i = 0; i < this.info.Count; i++) + for (int i = 0; i < this.data.Count; i++) { - GlyphLayoutData g = this.info[i]; + GlyphLayoutData g = this.data[i]; if (run != g.BidiRun) { run = g.BidiRun; @@ -1051,9 +1094,9 @@ public TextLine BidiReOrder() // https://unicode.org/reports/tr9/#L2 int max = 0; int min = int.MaxValue; - for (int i = 0; i < this.info.Count; i++) + for (int i = 0; i < this.data.Count; i++) { - int level = this.info[i].BidiRun.Level; + int level = this.data[i].BidiRun.Level; if (level > max) { max = level; @@ -1094,11 +1137,11 @@ public TextLine BidiReOrder() minLevelToReverse--; } - this.info.Clear(); + this.data.Clear(); current = orderedRun; while (current != null) { - this.info.AddRange(current.AsSlice()); + this.data.AddRange(current.AsSlice()); current = current.Next; } @@ -1176,6 +1219,7 @@ public GlyphLayoutData( float scaledLineHeight, float scaledAscender, float scaledDescender, + float scaledLineGap, BidiRun bidiRun, int graphemeIndex, int offset) @@ -1186,7 +1230,7 @@ public GlyphLayoutData( this.ScaledLineHeight = scaledLineHeight; this.ScaledAscender = scaledAscender; this.ScaledDescender = scaledDescender; - this.ScaledLineGap = scaledLineHeight - (scaledAscender + scaledDescender); + this.ScaledLineGap = scaledLineGap; this.BidiRun = bidiRun; this.GraphemeIndex = graphemeIndex; this.Offset = offset; diff --git a/src/SixLabors.Fonts/TextMeasurer.cs b/src/SixLabors.Fonts/TextMeasurer.cs index bcc4217d2..590119cae 100644 --- a/src/SixLabors.Fonts/TextMeasurer.cs +++ b/src/SixLabors.Fonts/TextMeasurer.cs @@ -104,7 +104,7 @@ internal static FontRectangle GetSize(IReadOnlyList glyphLayouts, f GlyphLayout glyph = glyphLayouts[i]; Vector2 location = glyph.Location; float x = location.X; - float y = location.Y - (glyph.Ascender + glyph.LineGap); + float y = location.Y - glyph.Ascender; float lineHeight = glyph.LineHeight; // Avoid trimming zero-width/height marks that extend past the bounds of their base. diff --git a/src/SixLabors.Fonts/TextRun.cs b/src/SixLabors.Fonts/TextRun.cs index ba3c107ab..af3a31a9b 100644 --- a/src/SixLabors.Fonts/TextRun.cs +++ b/src/SixLabors.Fonts/TextRun.cs @@ -8,17 +8,17 @@ namespace SixLabors.Fonts { /// - /// Represents a run of text spanning a series of code points within a string. + /// Represents a run of text spanning a series of graphemes within a string. /// public class TextRun { /// - /// Gets or sets the inclusive start index of the first codepoint in this . + /// Gets or sets the inclusive start index of the first grapheme in this . /// public int Start { get; set; } /// - /// Gets or sets the exclusive end index of the last codepoint in this . + /// Gets or sets the exclusive end index of the last grapheme in this . /// public int End { get; set; } @@ -47,27 +47,32 @@ public ReadOnlySpan Slice(ReadOnlySpan text) { ValidateRange(this.Start, this.End); - // Convert code point indices into char indices so we can slice + // Convert grapheme indices into char indices so we can slice int chars = 0; int count = 0; int start = 0; int length = 0; - SpanCodePointEnumerator codePointEnumerator = new(text); - while (codePointEnumerator.MoveNext()) + SpanGraphemeEnumerator graphemeEnumerator = new(text); + while (graphemeEnumerator.MoveNext()) { - if (count == this.Start) + SpanCodePointEnumerator codePointEnumerator = new(graphemeEnumerator.Current); + while (codePointEnumerator.MoveNext()) { - start = chars; - } + if (count == this.Start) + { + start = chars; + } - chars += codePointEnumerator.Current.Utf16SequenceLength; - length = chars - start; - if (++count == this.End) - { - break; + chars += codePointEnumerator.Current.Utf16SequenceLength; + length = chars - start; + if (++count == this.End) + { + goto End; + } } } + End: return text.Slice(start, length); } diff --git a/tests/SixLabors.Fonts.Tests/Tables/AdvancedTypographic/GPos/GPosTableTests.cs b/tests/SixLabors.Fonts.Tests/Tables/AdvancedTypographic/GPos/GPosTableTests.cs index cc0ce26bc..78b05c71a 100644 --- a/tests/SixLabors.Fonts.Tests/Tables/AdvancedTypographic/GPos/GPosTableTests.cs +++ b/tests/SixLabors.Fonts.Tests/Tables/AdvancedTypographic/GPos/GPosTableTests.cs @@ -21,8 +21,8 @@ public void SingleAdjustmentPositioning_Format1_Works() int[] expectedGlyphIndices = { 20, 22 }; FontRectangle[] expectedFontRectangles = { - new(275, 2011, 825, 719), - new(1622, 2010, 978, 704), + new(275, 1811, 825, 719), + new(1622, 1810, 978, 704), }; // act @@ -56,8 +56,8 @@ public void SingleAdjustmentPositioning_Format2_Works() int[] expectedGlyphIndices = { 23, 22 }; FontRectangle[] expectedFontRectangles = { - new(322, 2011, 978, 708), - new(1522, 2010, 978, 704), + new(322, 1811, 978, 708), + new(1522, 1810, 978, 704), }; // act @@ -91,9 +91,9 @@ public void PairAdjustmentPositioning_Format1_Works() int[] expectedGlyphIndices = { 25, 20, 22 }; FontRectangle[] expectedFontRectangles = { - new(322, 2030, 978, 671), - new(1675, 2011, 825, 719), - new(3322, 2410, 978, 704), + new(322, 1830, 978, 671), + new(1675, 1811, 825, 719), + new(3322, 2210, 978, 704), }; // act @@ -127,8 +127,8 @@ public void CursiveAttachmentPositioning_Format1_Works() int[] expectedGlyphIndices = { 20, 20 }; FontRectangle[] expectedFontRectangles = { - new(475, 2111, 825, 719), - new(575, 2011, 825, 719), + new(475, 1911, 825, 719), + new(575, 1811, 825, 719), }; // act @@ -162,8 +162,8 @@ public void MarkToBaseAttachmentPositioning_Format1_Works() int[] expectedGlyphIndices = { 20, 21 }; FontRectangle[] expectedFontRectangles = { - new(475, 2011, 825, 719), - new(375, 2090, 825, 709), + new(475, 1811, 825, 719), + new(375, 1890, 825, 709), }; // act @@ -197,8 +197,8 @@ public void MarkToLigatureAttachmentPositioning_Format1_Works() int[] expectedGlyphIndices = { 20, 21 }; FontRectangle[] expectedFontRectangles = { - new(475, 2011, 825, 719), - new(375, 2090, 825, 709), + new(475, 1811, 825, 719), + new(375, 1890, 825, 709), }; // act @@ -232,8 +232,8 @@ public void MarkToMarkAttachmentPositioning_Format1_Works() int[] expectedGlyphIndices = { 20, 21 }; FontRectangle[] expectedFontRectangles = { - new(475, 2011, 825, 719), - new(375, 2090, 825, 709), + new(475, 1811, 825, 719), + new(375, 1890, 825, 709), }; // act @@ -267,9 +267,9 @@ public void ContextualPositioning_Format1_Works() int[] expectedGlyphIndices = { 22, 23, 24 }; FontRectangle[] expectedFontRectangles = { - new(342, 2010, 978, 704), - new(1842, 2011, 978, 708), - new(3342, 2034, 978, 667), + new(342, 1810, 978, 704), + new(1842, 1811, 978, 708), + new(3342, 1834, 978, 667), }; // act @@ -303,9 +303,9 @@ public void ContextualPositioning_Format2_Works() int[] expectedGlyphIndices = { 22, 23, 24 }; FontRectangle[] expectedFontRectangles = { - new(342, 2010, 978, 704), - new(1842, 2011, 978, 708), - new(3342, 2034, 978, 667), + new(342, 1810, 978, 704), + new(1842, 1811, 978, 708), + new(3342, 1834, 978, 667), }; // act @@ -340,9 +340,9 @@ public void ContextualPositioning_Format3_Works() FontRectangle[] expectedFontRectangles = { - new(342, 2010, 978, 704), - new(1842, 2011, 978, 708), - new(3342, 2034, 978, 667), + new(342, 1810, 978, 704), + new(1842, 1811, 978, 708), + new(3342, 1834, 978, 667), }; // act @@ -378,10 +378,10 @@ public void ChainedContextsPositioning_Format1_Works() int[] expectedGlyphIndices = { 22, 23, 24, 25 }; FontRectangle[] expectedFontRectangles = { - new(322, 2010, 978, 704), - new(2022, 2011, 978, 708), - new(3522, 2034, 978, 667), - new(4822, 2030, 978, 671), + new(322, 1810, 978, 704), + new(2022, 1811, 978, 708), + new(3522, 1834, 978, 667), + new(4822, 1830, 978, 671), }; // act @@ -417,10 +417,10 @@ public void ChainedContextsPositioning_Format2_Works() int[] expectedGlyphIndices = { 22, 23, 24, 25 }; FontRectangle[] expectedFontRectangles = { - new(322, 2010, 978, 704), - new(2022, 2011, 978, 708), - new(3522, 2034, 978, 667), - new(4822, 2030, 978, 671), + new(322, 1810, 978, 704), + new(2022, 1811, 978, 708), + new(3522, 1834, 978, 667), + new(4822, 1830, 978, 671), }; // act @@ -456,10 +456,10 @@ public void ChainedContextsPositioning_Format3_Works() int[] expectedGlyphIndices = { 22, 23, 24, 25 }; FontRectangle[] expectedFontRectangles = { - new(322, 2010, 978, 704), - new(2022, 2011, 978, 708), - new(3522, 2034, 978, 667), - new(4822, 2030, 978, 671), + new(322, 1810, 978, 704), + new(2022, 1811, 978, 708), + new(3522, 1834, 978, 667), + new(4822, 1830, 978, 671), }; // act @@ -492,8 +492,8 @@ public void MarkAnchoring_Works() int[] expectedGlyphIndices = { 759, 989 }; FontRectangle[] expectedFontRectangles = { - new(246, 264, 363, 322), - new(71, 325, 966, 1573), + new(246, 177, 363, 322), + new(71, 238, 966, 1573), }; // act diff --git a/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs b/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs index 291fd814a..97c9be22e 100644 --- a/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs +++ b/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs @@ -251,12 +251,12 @@ public void MeasureTextWordWrappingVerticalRightLeft(string text, float height, #if OS_WINDOWS [Theory] - [InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious Taumatawhakatangihangakoauauotamateaturipukakapikimaungahoronukupokaiwhenuakitanatahu グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalTopBottom, WordBreaking.Normal, 125, 871)] - [InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious Taumatawhakatangihangakoauauotamateaturipukakapikimaungahoronukupokaiwhenuakitanatahu グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalTopBottom, WordBreaking.BreakAll, 152, 400)] - [InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalTopBottom, WordBreaking.KeepAll, 76, 700)] - [InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious Taumatawhakatangihangakoauauotamateaturipukakapikimaungahoronukupokaiwhenuakitanatahu グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalBottomTop, WordBreaking.Normal, 125, 871)] - [InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious Taumatawhakatangihangakoauauotamateaturipukakapikimaungahoronukupokaiwhenuakitanatahu グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalBottomTop, WordBreaking.BreakAll, 152, 400)] - [InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalBottomTop, WordBreaking.KeepAll, 76, 700)] + [InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious Taumatawhakatangihangakoauauotamateaturipukakapikimaungahoronukupokaiwhenuakitanatahu グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalTopBottom, WordBreaking.Normal, 134, 871)] + [InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious Taumatawhakatangihangakoauauotamateaturipukakapikimaungahoronukupokaiwhenuakitanatahu グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalTopBottom, WordBreaking.BreakAll, 160, 400)] + [InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalTopBottom, WordBreaking.KeepAll, 80, 700)] + [InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious Taumatawhakatangihangakoauauotamateaturipukakapikimaungahoronukupokaiwhenuakitanatahu グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalBottomTop, WordBreaking.Normal, 134, 871)] + [InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious Taumatawhakatangihangakoauauotamateaturipukakapikimaungahoronukupokaiwhenuakitanatahu グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalBottomTop, WordBreaking.BreakAll, 160, 400)] + [InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalBottomTop, WordBreaking.KeepAll, 80, 700)] public void MeasureTextWordBreak(string text, LayoutMode layoutMode, WordBreaking wordBreaking, float height, float width) { // Testing using Windows only to ensure that actual glyphs are rendered From cfec9a509da64fd0af0239a52a46ecb35f92fcbd Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 21 Mar 2022 12:36:04 +1100 Subject: [PATCH 46/48] Tweak adjusted advance ratio based on testing --- src/SixLabors.Fonts/TextLayout.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/SixLabors.Fonts/TextLayout.cs b/src/SixLabors.Fonts/TextLayout.cs index bd8a12dc9..bf6f5468b 100644 --- a/src/SixLabors.Fonts/TextLayout.cs +++ b/src/SixLabors.Fonts/TextLayout.cs @@ -259,12 +259,12 @@ private static IEnumerable LayoutLineHorizontal( // Recalculate the advance based upon the next line. // If larger, we want to scale it up to ensure it it pushed down far enough. - // We split the different exactly to line up the text at the exact vertical center. + // We split the different at 2/3 (heuristically determined value based upon extensive visual testing). if (!isFirstLine && !isLastLine) { TextLine next = textBox.TextLines[index + 1]; float nextLineAdvance = textBox.ScaledMaxLineHeight(next.MaxPointSize) * options.LineSpacing; - scaledLineAdvance += (nextLineAdvance - scaledLineAdvance) * .5F; + scaledLineAdvance += (nextLineAdvance - scaledLineAdvance) * .667F; } float originX = location.X; @@ -288,7 +288,7 @@ private static IEnumerable LayoutLineHorizontal( { TextLine next = textBox.TextLines[index + 1]; float nextLineAdvance = textBox.ScaledMaxLineHeight(next.MaxPointSize); - advance += (nextLineAdvance - advance) * .5F; + advance += (nextLineAdvance - advance) * .667F; } offsetY -= advance * options.LineSpacing * .5F; @@ -304,7 +304,7 @@ private static IEnumerable LayoutLineHorizontal( { TextLine next = textBox.TextLines[index + 1]; float nextLineAdvance = textBox.ScaledMaxLineHeight(next.MaxPointSize); - advance += (nextLineAdvance - advance) * .5F; + advance += (nextLineAdvance - advance) * .667F; } offsetY -= advance * options.LineSpacing; From 6c3d00b28fdcc683b756dad7d8d5f73415775efe Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 24 Mar 2022 22:23:21 +1100 Subject: [PATCH 47/48] Fix font hinting --- src/SixLabors.Fonts/GlyphMetrics.cs | 25 +++++++++++++++++----- src/SixLabors.Fonts/Hinting/Interpreter.cs | 5 +---- src/SixLabors.Fonts/StreamFontMetrics.cs | 23 +++++++++----------- 3 files changed, 31 insertions(+), 22 deletions(-) diff --git a/src/SixLabors.Fonts/GlyphMetrics.cs b/src/SixLabors.Fonts/GlyphMetrics.cs index 8d5c1ac1e..945a03e2b 100644 --- a/src/SixLabors.Fonts/GlyphMetrics.cs +++ b/src/SixLabors.Fonts/GlyphMetrics.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Numerics; +using SixLabors.Fonts.Tables.General; using SixLabors.Fonts.Tables.General.Glyphs; using SixLabors.Fonts.Unicode; @@ -239,6 +240,13 @@ internal void RenderTo(IGlyphRenderer surface, float pointSize, Vector2 location float dpi = options.Dpi; location *= dpi; float scaledPoint = dpi * pointSize; + bool forcePPEMToInt = (this.FontMetrics.HeadFlags & HeadTable.HeadFlags.ForcePPEMToInt) != 0; + + if (forcePPEMToInt) + { + scaledPoint = MathF.Round(scaledPoint); + } + FontRectangle box = this.GetBoundingBox(location, scaledPoint); // TextRun is never null here as rendering is only accessable via a Glyph which @@ -261,12 +269,19 @@ internal void RenderTo(IGlyphRenderer surface, float pointSize, Vector2 location var transform = Matrix3x2.CreateScale(scale); transform.Translation = this.offset * scale * MirrorScale; scaledVector = GlyphVector.Transform(this.vector, transform); - this.scaledVector[scaledPoint] = scaledVector; - } - if (options.ApplyHinting) - { - this.FontMetrics.ApplyHinting(scaledVector, pointSize * dpi / 72, this.GlyphId); + if (options.ApplyHinting) + { + float scaledPixel = scaledPoint * (dpi / 72F); + if (forcePPEMToInt) + { + scaledPixel = MathF.Round(scaledPixel); + } + + this.FontMetrics.ApplyHinting(this, ref scaledVector, scale, scaledPixel); + } + + this.scaledVector[scaledPoint] = scaledVector; } GlyphOutline outline = scaledVector.GetOutline(); diff --git a/src/SixLabors.Fonts/Hinting/Interpreter.cs b/src/SixLabors.Fonts/Hinting/Interpreter.cs index ea9344c25..1f264c134 100644 --- a/src/SixLabors.Fonts/Hinting/Interpreter.cs +++ b/src/SixLabors.Fonts/Hinting/Interpreter.cs @@ -100,9 +100,6 @@ public void HintGlyph(GlyphTableEntry glyphVector) if ((state.InstructionControl & InstructionControlFlags.InhibitGridFitting) != 0) return; - // TODO: composite glyphs - // TODO: round the phantom points? - // save contours and points this.contours = glyphVector.EndPoints; zp0 = zp1 = zp2 = points = new Zone(glyphVector, isTwilight: false); @@ -128,7 +125,7 @@ public void HintGlyph(GlyphTableEntry glyphVector) Execute(new StackInstructionStream(instructions, 0), false, false); } - private System.Collections.Generic.List debugList = new System.Collections.Generic.List(); + private List debugList = new List(); private void Execute(StackInstructionStream stream, bool inFunction, bool allowFunctionDefs) { diff --git a/src/SixLabors.Fonts/StreamFontMetrics.cs b/src/SixLabors.Fonts/StreamFontMetrics.cs index f210b2681..d1bc076ae 100644 --- a/src/SixLabors.Fonts/StreamFontMetrics.cs +++ b/src/SixLabors.Fonts/StreamFontMetrics.cs @@ -180,8 +180,11 @@ internal StreamFontMetrics( this.prep = prep; this.glyphDefinitionTable = glyphDefinitionTable; this.Description = new FontDescription(nameTable, os2, head); + this.HeadFlags = head.Flags; } + public HeadTable.HeadFlags HeadFlags { get; } + /// public override FontDescription Description { get; } @@ -475,7 +478,7 @@ public static StreamFontMetrics[] LoadFontCollection(Stream stream) return fonts; } - internal GlyphVector ApplyHinting(GlyphVector glyphVector, float pixelSize, ushort glyphIndex) + internal void ApplyHinting(GlyphMetrics metrics, ref GlyphVector glyphVector, Vector2 scaleXY, float scaledPixel) { if (this.interpreter == null) { @@ -492,23 +495,17 @@ internal GlyphVector ApplyHinting(GlyphVector glyphVector, float pixelSize, usho } } - float scale = pixelSize / this.UnitsPerEm; - this.interpreter.SetControlValueTable(this.cvt?.ControlValues, scale, pixelSize, this.prep?.Instructions); + float scaleFactor = scaledPixel / this.UnitsPerEm; + this.interpreter.SetControlValueTable(this.cvt?.ControlValues, scaleFactor, scaledPixel, this.prep?.Instructions); Bounds bounds = glyphVector.GetBounds(); - short leftSideBearing = this.horizontalMetrics.GetLeftSideBearing(glyphIndex); - ushort advanceWidth = this.horizontalMetrics.GetAdvancedWidth(glyphIndex); - short topSideBearing = this.verticalMetricsTable?.GetTopSideBearing(glyphIndex) ?? (short)(this.Ascender - bounds.Max.Y); - ushort advanceHeight = this.verticalMetricsTable?.GetAdvancedHeight(glyphIndex) ?? (ushort)this.LineHeight; - var pp1 = new Vector2(bounds.Min.X - (leftSideBearing * scale), 0); - var pp2 = new Vector2(pp1.X + (advanceWidth * scale), 0); - var pp3 = new Vector2(0, bounds.Max.Y + (topSideBearing * scale)); - var pp4 = new Vector2(0, pp3.Y - (advanceHeight * scale)); + var pp1 = new Vector2(bounds.Min.X - (metrics.LeftSideBearing * scaleXY.X), 0); + var pp2 = new Vector2(pp1.X + (metrics.AdvanceWidth * scaleXY.X), 0); + var pp3 = new Vector2(0, bounds.Max.Y + (metrics.TopSideBearing * scaleXY.Y)); + var pp4 = new Vector2(0, pp3.Y - (metrics.AdvanceHeight * scaleXY.Y)); GlyphVector.Hint(ref glyphVector, this.interpreter, pp1, pp2, pp3, pp4); - - return glyphVector; } private bool TryGetColoredMetrics(CodePoint codePoint, ushort glyphId, [NotNullWhen(true)] out GlyphMetrics[]? metrics) From 7b3ec0894e30e5ece613a8bb539c8e059cc04166 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sat, 26 Mar 2022 23:30:22 +1100 Subject: [PATCH 48/48] Don't upscale already scaled ppem --- src/SixLabors.Fonts/GlyphMetrics.cs | 22 +++++++------------ src/SixLabors.Fonts/StreamFontMetrics.cs | 6 ++--- .../Tables/General/Glyphs/GlyphVector.cs | 2 ++ 3 files changed, 13 insertions(+), 17 deletions(-) diff --git a/src/SixLabors.Fonts/GlyphMetrics.cs b/src/SixLabors.Fonts/GlyphMetrics.cs index 945a03e2b..67dbdc404 100644 --- a/src/SixLabors.Fonts/GlyphMetrics.cs +++ b/src/SixLabors.Fonts/GlyphMetrics.cs @@ -239,15 +239,15 @@ internal void RenderTo(IGlyphRenderer surface, float pointSize, Vector2 location { float dpi = options.Dpi; location *= dpi; - float scaledPoint = dpi * pointSize; + float scaledPPEM = dpi * pointSize; bool forcePPEMToInt = (this.FontMetrics.HeadFlags & HeadTable.HeadFlags.ForcePPEMToInt) != 0; if (forcePPEMToInt) { - scaledPoint = MathF.Round(scaledPoint); + scaledPPEM = MathF.Round(scaledPPEM); } - FontRectangle box = this.GetBoundingBox(location, scaledPoint); + FontRectangle box = this.GetBoundingBox(location, scaledPPEM); // TextRun is never null here as rendering is only accessable via a Glyph which // uses the cloned metrics instance. @@ -262,26 +262,20 @@ internal void RenderTo(IGlyphRenderer surface, float pointSize, Vector2 location colorSurface.SetColor(this.GlyphColor.Value); } - if (!this.scaledVector.TryGetValue(scaledPoint, out GlyphVector scaledVector)) + if (!this.scaledVector.TryGetValue(scaledPPEM, out GlyphVector scaledVector)) { // Scale and translate the glyph - Vector2 scale = new Vector2(scaledPoint) / this.ScaleFactor; + Vector2 scale = new Vector2(scaledPPEM) / this.ScaleFactor; var transform = Matrix3x2.CreateScale(scale); transform.Translation = this.offset * scale * MirrorScale; scaledVector = GlyphVector.Transform(this.vector, transform); if (options.ApplyHinting) { - float scaledPixel = scaledPoint * (dpi / 72F); - if (forcePPEMToInt) - { - scaledPixel = MathF.Round(scaledPixel); - } - - this.FontMetrics.ApplyHinting(this, ref scaledVector, scale, scaledPixel); + this.FontMetrics.ApplyHinting(this, ref scaledVector, scale, scaledPPEM); } - this.scaledVector[scaledPoint] = scaledVector; + this.scaledVector[scaledPPEM] = scaledVector; } GlyphOutline outline = scaledVector.GetOutline(); @@ -360,7 +354,7 @@ internal void RenderTo(IGlyphRenderer surface, float pointSize, Vector2 location (Vector2 Start, Vector2 End, float Thickness) GetEnds(float thickness, float position) { - Vector2 scale = new Vector2(scaledPoint) / this.ScaleFactor * MirrorScale; + Vector2 scale = new Vector2(scaledPPEM) / this.ScaleFactor * MirrorScale; Vector2 offset = location + (this.offset * scale * MirrorScale); // Calculate the correct advance for the line. diff --git a/src/SixLabors.Fonts/StreamFontMetrics.cs b/src/SixLabors.Fonts/StreamFontMetrics.cs index d1bc076ae..628f69526 100644 --- a/src/SixLabors.Fonts/StreamFontMetrics.cs +++ b/src/SixLabors.Fonts/StreamFontMetrics.cs @@ -478,7 +478,7 @@ public static StreamFontMetrics[] LoadFontCollection(Stream stream) return fonts; } - internal void ApplyHinting(GlyphMetrics metrics, ref GlyphVector glyphVector, Vector2 scaleXY, float scaledPixel) + internal void ApplyHinting(GlyphMetrics metrics, ref GlyphVector glyphVector, Vector2 scaleXY, float scaledPPEM) { if (this.interpreter == null) { @@ -495,8 +495,8 @@ internal void ApplyHinting(GlyphMetrics metrics, ref GlyphVector glyphVector, Ve } } - float scaleFactor = scaledPixel / this.UnitsPerEm; - this.interpreter.SetControlValueTable(this.cvt?.ControlValues, scaleFactor, scaledPixel, this.prep?.Instructions); + float scaleFactor = scaledPPEM / this.UnitsPerEm; + this.interpreter.SetControlValueTable(this.cvt?.ControlValues, scaleFactor, scaledPPEM, this.prep?.Instructions); Bounds bounds = glyphVector.GetBounds(); diff --git a/src/SixLabors.Fonts/Tables/General/Glyphs/GlyphVector.cs b/src/SixLabors.Fonts/Tables/General/Glyphs/GlyphVector.cs index 6a333c248..346af76b3 100644 --- a/src/SixLabors.Fonts/Tables/General/Glyphs/GlyphVector.cs +++ b/src/SixLabors.Fonts/Tables/General/Glyphs/GlyphVector.cs @@ -133,6 +133,8 @@ public static void Hint(ref GlyphVector glyph, Interpreter interpreter, Vector2 for (int i = 0; i < glyph.entries.Count; i++) { GlyphTableEntry entry = glyph.entries[i]; + + // TODO: Figure out a way to pool this. var controlPoints = new Vector2[entry.ControlPoints.Length + 4]; controlPoints[controlPoints.Length - 4] = pp1; controlPoints[controlPoints.Length - 3] = pp2;