diff --git a/samples/DrawWithImageSharp/Program.cs b/samples/DrawWithImageSharp/Program.cs index f1735e38d..85730cf2c 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"); @@ -40,15 +42,35 @@ 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(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); + + string arabic = "بِسْمِ ٱللَّهِ ٱلرَّحْمَٟنِ ٱلرَّحِيمِ"; + textRuns = new List + { + 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 = TextDecorations.Underline } + }; + RenderText(bugzilla, arabic, pointSize: 72, textRuns: textRuns); + RenderText(font2, "\uFB01", pointSize: 11.25F); RenderText(fontWoff2, "\uFB01", pointSize: 11.25F); 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); RenderText(uiFont, "first\n\n\n\nl", pointSize: 20, fallbackFonts: new[] { font2 }); @@ -182,12 +204,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() + // WrappingLength = 400, + FallbackFontFamilies = fallbackFonts?.ToArray(), + TextRuns = textRuns?.ToArray() }, text); 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; } /// diff --git a/src/SixLabors.Fonts/FileFontMetrics.cs b/src/SixLabors.Fonts/FileFontMetrics.cs index 6e9f5770a..8acd655ea 100644 --- a/src/SixLabors.Fonts/FileFontMetrics.cs +++ b/src/SixLabors.Fonts/FileFontMetrics.cs @@ -70,6 +70,45 @@ 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; + + /// + 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/Font.cs b/src/SixLabors.Fonts/Font.cs index 0c0387fe0..31a9b2fbc 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, TextAttributes.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, TextAttributes 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/FontMetrics.cs b/src/SixLabors.Fonts/FontMetrics.cs index f6d7913ab..f473c25d9 100644 --- a/src/SixLabors.Fonts/FontMetrics.cs +++ b/src/SixLabors.Fonts/FontMetrics.cs @@ -67,6 +67,72 @@ 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 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/GlyphLayout.cs b/src/SixLabors.Fonts/GlyphLayout.cs index 4c72d8626..21fe94d7f 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 @@ -15,14 +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; @@ -33,26 +39,46 @@ internal GlyphLayout( /// public Glyph Glyph { get; } + /// + /// Gets the codepoint represented by this glyph. + /// + public CodePoint CodePoint { get; } + /// /// Gets the location. /// 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. /// @@ -62,7 +88,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 +105,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 7d00ec3db..67dbdc404 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; @@ -17,6 +18,8 @@ public class GlyphMetrics private static readonly Vector2 MirrorScale = new(1, -1); private GlyphVector vector; private readonly Dictionary scaledVector = new(); + private Vector2 offset = Vector2.Zero; + private TextRun? textRun; internal GlyphMetrics( StreamFontMetrics font, @@ -45,31 +48,13 @@ 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.ScaleFactor = this.UnitsPerEm * 72F; + this.BottomSideBearing = (short)(this.AdvanceHeight - this.TopSideBearing - this.Height); + this.ScaleFactor = new(unitsPerEM * 72F); this.GlyphColor = glyphColor; } - internal GlyphMetrics(GlyphMetrics other, 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.LeftSideBearing = other.LeftSideBearing; - this.TopSideBearing = other.TopSideBearing; - this.ScaleFactor = other.ScaleFactor; - this.GlyphColor = other.GlyphColor; - } - /// /// Gets the font metrics. /// @@ -95,11 +80,21 @@ internal GlyphMetrics(GlyphMetrics other, CodePoint codePoint) /// 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. /// @@ -124,13 +119,59 @@ internal GlyphMetrics(GlyphMetrics other, CodePoint codePoint) public ushort UnitsPerEm { get; } /// - public float 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(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(TextAttributes.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. /// @@ -177,10 +218,10 @@ internal void ApplyAdvance(short x, short y) internal FontRectangle GetBoundingBox(Vector2 origin, float scaledPointSize) { + 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) + this.offset) * scale * MirrorScale; loc = origin + loc; return new FontRectangle(loc.X, loc.Y, size.X, size.Y); @@ -194,107 +235,223 @@ 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 scaledPPEM = dpi * pointSize; + bool forcePPEMToInt = (this.FontMetrics.HeadFlags & HeadTable.HeadFlags.ForcePPEMToInt) != 0; - float scaledPoint = dpi * pointSize; + if (forcePPEMToInt) + { + scaledPPEM = MathF.Round(scaledPPEM); + } - FontRectangle box = this.GetBoundingBox(location, scaledPoint); + FontRectangle box = this.GetBoundingBox(location, scaledPPEM); - var parameters = new GlyphRendererParameters(this, 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)) { - 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)) - { - scaledVector = GlyphVector.Scale(this.vector, scaledPoint / this.ScaleFactor); - 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 (onCurves[endOfContour]) + if (this.GlyphColor.HasValue && surface is IColorGlyphRenderer colorSurface) { - surface.MoveTo(curr); + colorSurface.SetColor(this.GlyphColor.Value); } - else + + if (!this.scaledVector.TryGetValue(scaledPPEM, out GlyphVector scaledVector)) { - if (onCurves[startOfContour]) - { - surface.MoveTo(next); - } - else + // Scale and translate the glyph + 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) { - // If both first and last points are off-curve, start at their middle. - Vector2 startPoint = (curr + next) / 2; - surface.MoveTo(startPoint); + this.FontMetrics.ApplyHinting(this, ref scaledVector, scale, scaledPPEM); } + + this.scaledVector[scaledPPEM] = scaledVector; } - 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) * .5F; + 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) * .5F; + surface.LineTo(prev2); + } - surface.LineTo(prev2); - surface.QuadraticBezierTo(curr, next2); + if (!onCurves[nextIndex]) + { + next2 = (curr + next) * .5F; + } + + surface.LineTo(prev2); + surface.QuadraticBezierTo(curr, next2); + } } + + surface.EndFigure(); } + } + + (Vector2 Start, Vector2 End, float Thickness) GetEnds(float thickness, float position) + { + Vector2 scale = new Vector2(scaledPPEM) / this.ScaleFactor * MirrorScale; + Vector2 offset = location + (this.offset * scale * MirrorScale); + + // Calculate the correct advance for the line. + float width = this.AdvanceWidth; + if (width == 0) + { + // For zero advance glyphs we must calculate our advance width from bearing + width; + width = this.LeftSideBearing + this.Width; + } + + 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(); + + (Vector2 start, Vector2 end, float 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); + tr.Y = MathF.Ceiling(tr.Y); + br.Y = MathF.Floor(br.Y); + bl.Y = MathF.Floor(bl.Y); + + // Do the same for vertical components. + tl.X = MathF.Floor(tl.X); + tr.X = MathF.Floor(tr.X); + br.X = MathF.Floor(br.X); + bl.X = MathF.Floor(bl.X); + + surface.MoveTo(tl); + surface.LineTo(bl); + surface.LineTo(br); + surface.LineTo(tr); surface.EndFigure(); } + + void SetDecoration(TextDecorations decorationType, float thickness, float position) + { + (Vector2 start, Vector2 end, float calcThickness) = GetEnds(thickness, position); + ((IGlyphDecorationRenderer)surface).SetDecoration(decorationType, start, end, calcThickness); + } + + // 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 + TextDecorations decorations = decorationSurface.EnabledDecorations(); + if ((decorations & TextDecorations.Underline) == TextDecorations.Underline) + { + SetDecoration(TextDecorations.Underline, this.FontMetrics.UnderlineThickness, this.FontMetrics.UnderlinePosition); + } + + if ((decorations & TextDecorations.Strikeout) == TextDecorations.Strikeout) + { + SetDecoration(TextDecorations.Strikeout, this.FontMetrics.StrikeoutSize, this.FontMetrics.StrikeoutPosition); + } + + if ((decorations & TextDecorations.Overline) == TextDecorations.Overline) + { + 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 & TextDecorations.Underline) == TextDecorations.Underline) + { + DrawLine(this.FontMetrics.UnderlineThickness, this.FontMetrics.UnderlinePosition); + } + + if ((this.textRun!.TextDecorations & TextDecorations.Strikeout) == TextDecorations.Strikeout) + { + DrawLine(this.FontMetrics.StrikeoutSize, this.FontMetrics.StrikeoutPosition); + } + + if ((this.textRun!.TextDecorations & TextDecorations.Overline) == TextDecorations.Overline) + { + DrawLine(overlineThickness, overlinePosition); + } + } } surface.EndGlyph(); diff --git a/src/SixLabors.Fonts/GlyphPositioningCollection.cs b/src/SixLabors.Fonts/GlyphPositioningCollection.cs index 50b42378f..11a68bdce 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,30 +76,38 @@ 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; + } /// - /// 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 font face with metrics. /// The glyph substitution collection. /// if the metrics collection does not contain any fallbacks; otherwise . - public bool TryAddOrUpdate(FontMetrics fontMetrics, GlyphSubstitutionCollection collection) + public bool TryUpdate(Font font, GlyphSubstitutionCollection collection) { - if (this.Count == 0) - { - return this.Add(fontMetrics, collection); - } - + FontMetrics fontMetrics = font.FontMetrics; + ColorFontSupport colorFontSupport = this.TextOptions.ColorFontSupport; bool hasFallBacks = false; List orphans = new(); for (int i = 0; i < this.offsets.Count; i++) @@ -113,8 +121,8 @@ public bool TryAddOrUpdate(FontMetrics fontMetrics, GlyphSubstitutionCollection 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; @@ -122,13 +130,13 @@ public bool TryAddOrUpdate(FontMetrics fontMetrics, GlyphSubstitutionCollection CodePoint codePoint = data.CodePoint; ushort[] glyphIds = data.GlyphIds; - 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, colorFontSupport)) { if (gm.GlyphType == GlyphType.Fallback && !CodePoint.IsControl(codePoint)) { @@ -138,14 +146,16 @@ public bool TryAddOrUpdate(FontMetrics fontMetrics, GlyphSubstitutionCollection 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(GlyphMetrics.CloneForRendering(gm, data.TextRun, codePoint)); } } 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; } @@ -163,35 +173,46 @@ 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(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; - 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, colorFontSupport)) { if (gm.GlyphType == GlyphType.Fallback && !CodePoint.IsControl(codePoint)) { 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(GlyphMetrics.CloneForRendering(gm, data.TextRun, codePoint)); } } 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) }); @@ -216,19 +237,28 @@ private bool Add(FontMetrics fontMetrics, 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; } 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) { - 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); + } } } } @@ -244,7 +274,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) { @@ -260,6 +290,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/GlyphRendererParameters.cs b/src/SixLabors.Fonts/GlyphRendererParameters.cs index 50d9c390a..2413b524a 100644 --- a/src/SixLabors.Fonts/GlyphRendererParameters.cs +++ b/src/SixLabors.Fonts/GlyphRendererParameters.cs @@ -3,7 +3,7 @@ using System; using System.Diagnostics; -using System.Numerics; +using SixLabors.Fonts.Unicode; namespace SixLabors.Fonts { @@ -14,15 +14,17 @@ 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 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, float pointSize, float dpi) public string Font { get; } /// - /// Gets the type of this glyph. + /// Gets the color details of this glyph. /// public GlyphColor GlyphColor { get; } @@ -41,25 +43,35 @@ internal GlyphRendererParameters(GlyphMetrics glyph, float pointSize, float dpi) 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 dpi along the X axis we are rendering at. + /// Gets the dots-per-inch the glyph is to be rendered at. /// public float Dpi { get; } + /// + /// Gets the text run this glyph belongs to. + /// + public TextRun TextRun { get; } + /// /// Compares two objects for equality. /// @@ -97,6 +109,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)); @@ -113,6 +126,7 @@ public override int GetHashCode() this.GlyphType, this.GlyphColor, this.FontStyle, - this.Dpi); + this.Dpi, + this.TextRun); } } 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/GlyphShapingData.cs b/src/SixLabors.Fonts/GlyphShapingData.cs index a2b46bb6d..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,6 +30,7 @@ public GlyphShapingData(GlyphShapingData data, bool clearFeatures = false) this.CodePoint = data.CodePoint; this.CodePointCount = data.CodePointCount; this.Direction = data.Direction; + this.TextRun = data.TextRun; this.GlyphIds = data.GlyphIds; this.LigatureId = data.LigatureId; this.LigatureComponent = data.LigatureComponent; @@ -61,6 +60,11 @@ public GlyphShapingData(GlyphShapingData data, bool clearFeatures = false) /// public TextDirection Direction { get; set; } + /// + /// Gets or sets the text run this glyph belongs to. + /// + public TextRun TextRun { get; set; } + /// /// Gets or sets the collection of glyph ids. /// @@ -98,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.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 e1aac13ba..509f0d7e8 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; } @@ -94,10 +94,11 @@ 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 run this glyph belongs to. /// 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, TextRun textRun, int offset) { - this.glyphs.Add(offset, new() + this.glyphs.Add(offset, new(textRun) { CodePoint = codePoint, Direction = direction, @@ -109,13 +110,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/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/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/src/SixLabors.Fonts/IGlyphDecorationRenderer.cs b/src/SixLabors.Fonts/IGlyphDecorationRenderer.cs new file mode 100644 index 000000000..14465154a --- /dev/null +++ b/src/SixLabors.Fonts/IGlyphDecorationRenderer.cs @@ -0,0 +1,29 @@ +// 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 TextDecorations EnabledDecorations(); + + /// + /// Provides the positions required for drawing text decorations onto the + /// + /// The type of decoration these details correspond to. + /// The start position from where to draw the decorations from. + /// The end position from where to draw the decorations to. + /// The thickness to draw the decoration. + public void SetDecoration(TextDecorations textDecorations, Vector2 start, Vector2 end, float thickness); + } +} diff --git a/src/SixLabors.Fonts/StreamFontMetrics.cs b/src/SixLabors.Fonts/StreamFontMetrics.cs index bc8363ad5..628f69526 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, @@ -152,6 +155,21 @@ 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.UnderlinePosition = postTable.UnderlinePosition; + this.UnderlineThickness = postTable.UnderlineThickness; + this.ItalicAngle = postTable.ItalicAngle; + this.kerningTable = kern; this.gSubTable = gSubTable; this.gPosTable = gPosTable; @@ -162,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; } @@ -191,6 +212,45 @@ 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; } + + /// + 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 _); @@ -203,24 +263,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); } /// @@ -242,12 +292,14 @@ internal override IEnumerable GetGlyphMetrics(CodePoint codePoint, } if (support == ColorFontSupport.MicrosoftColrFormat - && this.TryGetColoredVectors(codePoint, glyphId, out GlyphMetrics[]? metrics)) + && this.TryGetColoredMetrics(codePoint, glyphId, 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 is null) { this.glyphCache[glyphId] = new[] { @@ -263,22 +315,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) { @@ -356,9 +400,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) @@ -375,10 +423,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, @@ -391,6 +435,7 @@ internal static StreamFontMetrics LoadFont(FontReader reader) verticalMetrics, head, kern, + postTable, gSub, gPos, colrTable, @@ -433,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 scaledPPEM) { if (this.interpreter == null) { @@ -450,53 +495,48 @@ 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 = scaledPPEM / this.UnitsPerEm; + this.interpreter.SetControlValueTable(this.cvt?.ControlValues, scaleFactor, scaledPPEM, 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 TryGetColoredVectors(CodePoint codePoint, ushort glyphId, [NotNullWhen(true)] out GlyphMetrics[]? vectors) + private bool TryGetColoredMetrics(CodePoint codePoint, ushort glyphId, [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 is null) { 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, layer.PaletteIndex); + metrics[i] = this.CreateGlyphMetrics(codePoint, layer.GlyphId, GlyphType.ColrLayer, 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/Tables/General/Glyphs/GlyphVector.cs b/src/SixLabors.Fonts/Tables/General/Glyphs/GlyphVector.cs index 8859ce7d7..346af76b3 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. /// @@ -124,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; 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/Tables/General/Post/PostNameRecord.cs b/src/SixLabors.Fonts/Tables/General/Post/PostNameRecord.cs new file mode 100644 index 000000000..2c0f112c7 --- /dev/null +++ b/src/SixLabors.Fonts/Tables/General/Post/PostNameRecord.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..c63fdee52 --- /dev/null +++ b/src/SixLabors.Fonts/Tables/General/PostTable.cs @@ -0,0 +1,198 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Text; +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 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 PostNameRecord[] PostRecords { get; } + + 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, Encoding.ASCII); + } + + records[i] = new PostNameRecord(glyphNameIndex, name); + } + } + 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, + 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/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/src/SixLabors.Fonts/TextAttributes.cs b/src/SixLabors.Fonts/TextAttributes.cs new file mode 100644 index 000000000..310560df7 --- /dev/null +++ b/src/SixLabors.Fonts/TextAttributes.cs @@ -0,0 +1,29 @@ +// 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 TextAttributes + { + /// + /// No attributes are applied + /// + None = 0, + + /// + /// The text set slightly below the normal line of type. + /// + Subscript = 1 << 0, + + /// + /// The text set slightly above the normal line of type. + /// + Superscript = 1 << 1, + } +} diff --git a/src/SixLabors.Fonts/TextDecorations.cs b/src/SixLabors.Fonts/TextDecorations.cs new file mode 100644 index 000000000..cc84b3007 --- /dev/null +++ b/src/SixLabors.Fonts/TextDecorations.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 TextDecorations + { + /// + /// 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/TextLayout.cs b/src/SixLabors.Fonts/TextLayout.cs index 90604c347..bf6f5468b 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; @@ -14,48 +13,99 @@ 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) { return Array.Empty(); } - if (options.WrappingLength > 0) + TextBox textBox = ProcessText(text, options); + return LayoutText(textBox, options); + } + + public static IReadOnlyList BuildTextRuns(ReadOnlySpan text, TextOptions options) + { + if (options.TextRuns is null || options.TextRuns.Count == 0) { - // Trim trailing white spaces from the text - text = text.TrimEnd(null); + return new TextRun[] + { + new() + { + Start = 0, + End = CodePoint.GetCodePointCount(text), + Font = options.Font + } + }; } - // Check our string again after trimming. - if (text.IsEmpty) + int start = 0; + int end = CodePoint.GetCodePointCount(text); + List textRuns = new(); + foreach (TextRun textRun in options.TextRuns!.OrderBy(x => x.Start)) { - return Array.Empty(); + // 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; } - TextBox textBox = ProcessText(text, options); - return LayoutText(textBox, options); + // Add a final run if required. + if (start < end) + { + // Offset error by user, last index in input string + // instead of exclusive index. + if (start == end - 1) + { + int prevIndex = textRuns.Count - 1; + TextRun previous = textRuns[prevIndex]; + previous.End++; + } + else + { + 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. - 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(); - } + 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); @@ -82,23 +132,44 @@ 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. - if (!DoFontRun( - text, - options, - mainFont, - bidiRuns, - bidiMap, - substitutions, - positionings)) + IReadOnlyList textRuns = BuildTextRuns(text, 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, + bidiMap, + substitutions, + positionings)) + { + complete = false; + } + } + + if (!complete) { - foreach (FontMetrics font in fallbackFonts) + // Finally try our fallback fonts. + // We do a complete run here across the whole collection. + foreach (Font font in fallbackFonts) { - substitutions.Clear(layoutMode); + runIndex = 0; + ref int index = ref runIndex; if (DoFontRun( text, - options, + 0, + textRuns, + ref index, + true, font, bidiRuns, bidiMap, @@ -113,10 +184,14 @@ 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 (FontMetrics font in fallbackFonts) + foreach (TextRun textRun in textRuns) { - font.UpdatePositions(positionings); + textRun.Font!.FontMetrics.UpdatePositions(positionings); + } + + foreach (Font font in fallbackFonts) + { + font.FontMetrics.UpdatePositions(positionings); } return BreakLines(text, options, bidiRuns, bidiMap, positionings, layoutMode); @@ -126,36 +201,38 @@ private static IReadOnlyList LayoutText(TextBox textBox, TextOption { LayoutMode layoutMode = options.LayoutMode; List glyphs = new(); - Vector2 location = options.Origin / options.Dpi; - float maxScaledAdvance = textBox.TextLines.Max(x => x.ScaledLineAdvance()); + float maxScaledAdvance = textBox.ScaledMaxAdvance(); 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)); } } 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, glyphs.Count == 0, ref location)); + glyphs.AddRange(LayoutLineHorizontal(textBox, textBox.TextLines[i], direction, maxScaledAdvance, options, index++, ref location)); } } 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 { + int index = 0; 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, index++, ref location)); } } @@ -168,29 +245,71 @@ private static IEnumerable LayoutLineHorizontal( TextDirection direction, float maxScaledAdvance, TextOptions options, - bool first, + int index, ref Vector2 location) { + 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 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) * .667F; + } + float originX = location.X; float offsetY = 0; float offsetX = 0; - if (first) + + // Set the Y-Origin for the line. + if (isFirstLine) { - // 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; + offsetY = (scaledMaxAscender - (scaledMaxDescender + scaledMaxLineGap)) * .5F; + for (int i = index; i < textBox.TextLines.Count - 1; i++) + { + 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) * .667F; + } + + offsetY -= advance * options.LineSpacing * .5F; + } + break; case VerticalAlignment.Bottom: - offsetY = -scaledMaxDescender; - offsetY -= (textBox.TextLines.Count - 1) * textBox.ScaledMaxLineHeight * options.LineSpacing; + offsetY = -(scaledMaxDescender + scaledMaxLineGap); + for (int i = index; i < textBox.TextLines.Count - 1; i++) + { + 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) * .667F; + } + + offsetY -= advance * options.LineSpacing; + } + break; } @@ -214,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; } } @@ -226,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; } } @@ -239,31 +358,48 @@ 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 += textBox.ScaledMaxLineHeight * options.LineSpacing; + 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(data.PointSize) / metric.ScaleFactor; + float advanceX = data.ScaledAdvance; + float advanceY = metric.AdvanceHeight * scale.Y; + if (advanceX == 0) + { + advanceX = (metric.LeftSideBearing + metric.Width) * scale.X; + } + + if (advanceY == 0) + { + advanceY = (metric.TopSideBearing + metric.Height) * scale.Y; + } + glyphs.Add(new GlyphLayout( - new Glyph(metric, info.PointSize), + new Glyph(metric, data.PointSize), location, - info.ScaledAdvance, - metric.AdvanceHeight * (info.PointSize / metric.ScaleFactor), - textBox.ScaledMaxLineHeight * options.LineSpacing, + scaledMaxAscender, + scaledMaxDescender, + scaledMaxLineGap, + scaledLineAdvance, + advanceX, + advanceY, i == 0)); } - location.X += info.ScaledAdvance; + location.X += data.ScaledAdvance; } location.X = originX; if (glyphs.Count > 0) { - location.Y += textBox.ScaledMaxLineHeight * options.LineSpacing; + location.Y += scaledLineAdvance; } return glyphs; @@ -275,28 +411,29 @@ private static IEnumerable LayoutLineVertical( TextDirection direction, float maxScaledAdvance, TextOptions options, - bool first, + int index, ref Vector2 location) { float originY = location.Y; 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 = 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) { case VerticalAlignment.Top: - offsetY = scaledMaxAscender; + offsetY = 0; break; case VerticalAlignment.Center: - offsetY = (scaledMaxAscender * .5F) - (scaledMaxDescender * .5F); - offsetY -= (maxScaledAdvance - textBox.ScaledMaxLineHeight) * .5F; + offsetY -= maxScaledAdvance * .5F; break; case VerticalAlignment.Bottom: - offsetY = -scaledMaxDescender; - offsetY -= maxScaledAdvance - textBox.ScaledMaxLineHeight; + offsetY -= maxScaledAdvance; break; } @@ -306,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; } } @@ -318,28 +455,36 @@ 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; } } location.Y += offsetY; - if (first) + bool isFirstLine = index == 0; + if (isFirstLine) { // Set the X-Origin for horizontal alignment. switch (options.HorizontalAlignment) { case HorizontalAlignment.Right: - offsetX = -(textBox.ScaledMaxLineHeight * options.LineSpacing); - offsetX -= (textBox.TextLines.Count - 1) * textBox.ScaledMaxLineHeight * options.LineSpacing; + // 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.ScaledMaxLineHeight(textBox.TextLines[i].MaxPointSize) * options.LineSpacing; + } + break; case HorizontalAlignment.Center: - offsetX = -(textBox.ScaledMaxLineHeight * options.LineSpacing * .5F); - offsetX -= (textBox.TextLines.Count - 1) * textBox.ScaledMaxLineHeight * options.LineSpacing * .5F; + for (int i = 0; i < textBox.TextLines.Count; i++) + { + offsetX -= textBox.ScaledMaxLineHeight(textBox.TextLines[i].MaxPointSize) * options.LineSpacing * .5F; + } + break; } } @@ -347,36 +492,54 @@ 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 * (isFirstLine ? 1F : options.LineSpacing); + float xLineAdvance = scaledMaxLineHeight * options.LineSpacing; - if (first) + if (isFirstLine) { - xLineAdvance -= (xLineAdvance - textBox.ScaledMaxLineHeight) * .5F; + xLineAdvance -= (xLineAdvance - scaledMaxLineHeight) * .5F; } 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(data.PointSize) / metric.ScaleFactor; + float advanceX = xLineAdvance; + float advanceY = data.ScaledAdvance; + + // Advance Width & Height can be 0 which is fine for layout but not for measuring. + if (advanceX == 0) + { + advanceX = (metric.LeftSideBearing + metric.Width) * scale.X; + } + + if (advanceY == 0) + { + advanceY = (metric.TopSideBearing + metric.Height) * scale.Y; + } + glyphs.Add(new GlyphLayout( - new Glyph(metric, info.PointSize), - location + new Vector2((xWidth - (metric.AdvanceWidth * (info.PointSize / metric.ScaleFactor))) * .5F, 0), - xLineAdvance, - info.ScaledAdvance, - textBox.ScaledMaxLineHeight, + new Glyph(metric, data.PointSize), + location + new Vector2((xWidth - (metric.AdvanceWidth * scale.X)) * .5F, data.ScaledAscender), + scaledMaxAscender, + scaledMaxDescender, + scaledMaxLineGap, + scaledMaxLineHeight, + advanceX, + advanceY, i == 0)); } - location.Y += info.ScaledAdvance; + location.Y += data.ScaledAdvance; } location.Y = originY; @@ -390,16 +553,23 @@ private static IEnumerable LayoutLineVertical( private static bool DoFontRun( ReadOnlySpan text, - TextOptions options, - FontMetrics fontMetrics, + int start, + IReadOnlyList textRuns, + ref int textRun, + bool isFallbackRun, + Font font, 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 = 0; + int codePointIndex = start; int bidiRun = 0; var graphemeEnumerator = new SpanGraphemeEnumerator(text); for (graphemeIndex = 0; graphemeEnumerator.MoveNext(); graphemeIndex++) @@ -408,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); @@ -437,8 +612,8 @@ 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); - substitutions.AddGlyph(glyphId, current, (TextDirection)bidiRuns[bidiRun].Direction, codePointIndex); + font.FontMetrics.TryGetGlyphId(current, next, out ushort glyphId, out skipNextCodePoint); + substitutions.AddGlyph(glyphId, current, (TextDirection)bidiRuns[bidiRun].Direction, textRuns[textRun], codePointIndex); codePointIndex++; graphemeCodePointIndex++; @@ -446,11 +621,13 @@ private static bool DoFontRun( } // Apply the simple and complex substitutions. - // TODO: Investigate HarfBuzz normlizer. - SubstituteBidiMirrors(fontMetrics, substitutions); - fontMetrics.ApplySubstitution(substitutions); + // TODO: Investigate HarfBuzz normalizer. + SubstituteBidiMirrors(font.FontMetrics, substitutions); + font.FontMetrics.ApplySubstitution(substitutions); - return positionings.TryAddOrUpdate(fontMetrics, substitutions); + return !isFallbackRun + ? positionings.TryAdd(font, substitutions) + : positionings.TryUpdate(font, substitutions); } private static void SubstituteBidiMirrors(FontMetrics fontMetrics, GlyphSubstitutionCollection collection) @@ -503,18 +680,12 @@ 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; 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(); @@ -542,7 +713,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++; @@ -568,8 +739,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)) @@ -599,7 +771,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; @@ -634,7 +806,7 @@ private static TextBox BreakLines( { textLines.Add(textLine.BidiReOrder()); textLine = split; - lineAdvance = split.ScaledLineAdvance(); + lineAdvance = split.ScaledLineAdvance; } } else @@ -645,6 +817,17 @@ private static TextBox BreakLines( lineAdvance = 0; } } + else if (currentLineBreak.PositionWrap == codePointIndex) + { + // Exact length match. Check for CJK + 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. @@ -653,7 +836,7 @@ private static TextBox BreakLines( { textLines.Add(textLine.BidiReOrder()); textLine = split; - lineAdvance = split.ScaledLineAdvance(); + lineAdvance = split.ScaledLineAdvance; } } } @@ -690,30 +873,17 @@ 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; - - if (ascender > scaledMaxAscender) + float scaleY = pointSize / metric.ScaleFactor.Y; + float ascender = metric.FontMetrics.Ascender * scaleY; + if (metric.TopSideBearing < 0) { - scaledMaxAscender = ascender; + // Adjust for glyphs with a negative tsb. e.g. emoji. + ascender -= metric.TopSideBearing * scaleY; } - if (descender > scaledMaxDescender) - { - scaledMaxDescender = descender; - } - - if (lineHeight > scaledMaxLineHeight) - { - scaledMaxLineHeight = lineHeight; - } - - if (leftSideBearing > scaledMaxLeftSideBearing) - { - scaledMaxLeftSideBearing = leftSideBearing; - } + 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; @@ -721,6 +891,10 @@ private static TextBox BreakLines( metrics, pointSize, glyphAdvance, + lineHeight, + ascender, + descender, + lineGap, bidiRuns[bidiMap[codePointIndex]], graphemeIndex, codePointIndex); @@ -736,68 +910,87 @@ 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 TextBox(IReadOnlyList textLines) => this.TextLines = textLines; - public float ScaledMaxAscender { get; } + public IReadOnlyList TextLines { get; } - public float ScaledMaxDescender { get; } + // TODO: It would be very good to cache these. + public float ScaledMaxAdvance() + => this.TextLines.Max(x => x.ScaledLineAdvance); - public float ScaledMaxLineHeight { get; } + public float ScaledMaxLineHeight(float pointSize) + => this.TextLines.Where(x => x.MaxPointSize == pointSize).Max(x => x.ScaledMaxLineHeight); - public float ScaledMaxLineGap { get; } + public float ScaledMaxAscender(float pointSize) + => this.TextLines.Where(x => x.MaxPointSize == pointSize).Max(x => x.ScaledMaxAscender); - public float ScaledMaxLeftSideBearing { get; } + public float ScaledMaxDescender(float pointSize) + => this.TextLines.Where(x => x.MaxPointSize == pointSize).Max(x => x.ScaledMaxDescender); - public ReadOnlyCollection TextLines { get; } + public float ScaledMaxLineGap(float pointSize) + => this.TextLines.Where(x => x.MaxPointSize == pointSize).Max(x => x.ScaledMaxLineGap); public TextDirection TextDirection() => this.TextLines[0][0].TextDirection; } internal sealed class TextLine { - private readonly List info = new(); + private readonly List data = new(); + + public int Count => this.data.Count; + + public float MaxPointSize { get; private set; } = -1; - public int Count => this.info.Count; + public float ScaledLineAdvance { get; private set; } = 0; - public GlyphLayoutData this[int index] => this.info[index]; + public float ScaledMaxLineHeight { get; private set; } = -1; - public float ScaledLineAdvance() - => this.info.Sum(x => x.ScaledAdvance); + public float ScaledMaxAscender { get; private set; } = -1; + + public float ScaledMaxDescender { get; private set; } = -1; + + public float ScaledMaxLineGap { get; private set; } = -1; + + public GlyphLayoutData this[int index] => this.data[index]; public void Add( GlyphMetrics[] metrics, float pointSize, float scaledAdvance, + float scaledLineHeight, + float scaledAscender, + float scaledDescender, + float scaledLineGap, BidiRun bidiRun, int graphemeIndex, int offset) - => this.info.Add(new(metrics, pointSize, scaledAdvance, 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) { @@ -818,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++; @@ -832,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; } @@ -848,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; @@ -884,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; @@ -927,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; } @@ -1006,6 +1216,10 @@ public GlyphLayoutData( GlyphMetrics[] metrics, float pointSize, float scaledAdvance, + float scaledLineHeight, + float scaledAscender, + float scaledDescender, + float scaledLineGap, BidiRun bidiRun, int graphemeIndex, int offset) @@ -1013,6 +1227,10 @@ public GlyphLayoutData( this.Metrics = metrics; this.PointSize = pointSize; this.ScaledAdvance = scaledAdvance; + this.ScaledLineHeight = scaledLineHeight; + this.ScaledAscender = scaledAscender; + this.ScaledDescender = scaledDescender; + this.ScaledLineGap = scaledLineGap; this.BidiRun = bidiRun; this.GraphemeIndex = graphemeIndex; this.Offset = offset; @@ -1026,6 +1244,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 417859176..590119cae 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) { @@ -84,18 +94,50 @@ 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; - // Avoid trimming zero-width 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); + 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; + 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(x + glyph.Width, box.Right); + float advanceY = Math.Max(y + lineHeight, box.Top + box.Height); + + if (left > x) + { + left = x; + } + + if (top > y) + { + top = 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) @@ -156,77 +198,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/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/TextRenderer.cs b/src/SixLabors.Fonts/TextRenderer.cs index fb4da3178..40b3fade4 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,18 +52,13 @@ 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); foreach (GlyphLayout g in glyphsToRender) { - if (g.IsWhiteSpace()) - { - continue; - } - g.Glyph.RenderTo(this.renderer, g.Location, options); } diff --git a/src/SixLabors.Fonts/TextRun.cs b/src/SixLabors.Fonts/TextRun.cs new file mode 100644 index 000000000..af3a31a9b --- /dev/null +++ b/src/SixLabors.Fonts/TextRun.cs @@ -0,0 +1,97 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Runtime.CompilerServices; +using SixLabors.Fonts.Unicode; + +namespace SixLabors.Fonts +{ + /// + /// 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 grapheme in this . + /// + public int Start { get; set; } + + /// + /// Gets or sets the exclusive end index of the last grapheme 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 TextAttributes TextAttributes { get; set; } + + /// + /// Gets or sets the text decorations applied to this run. + /// + public TextDecorations TextDecorations { get; set; } + + /// + /// Returns the slice of the given text representing this . + /// + /// The text to slice. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ReadOnlySpan Slice(ReadOnlySpan text) + { + ValidateRange(this.Start, this.End); + + // Convert grapheme indices into char indices so we can slice + int chars = 0; + int count = 0; + int start = 0; + int length = 0; + SpanGraphemeEnumerator graphemeEnumerator = new(text); + while (graphemeEnumerator.MoveNext()) + { + SpanCodePointEnumerator codePointEnumerator = new(graphemeEnumerator.Current); + while (codePointEnumerator.MoveNext()) + { + if (count == this.Start) + { + start = chars; + } + + chars += codePointEnumerator.Current.Utf16SequenceLength; + length = chars - start; + if (++count == this.End) + { + goto End; + } + } + } + + End: + return text.Slice(start, length); + } + + /// + 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}'."); + } + } + } +} 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. 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/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()); } } 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 0af0e926c..ae454a024 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; @@ -12,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); @@ -25,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); } @@ -46,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); } @@ -53,20 +91,18 @@ 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); 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); - 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); @@ -87,13 +123,10 @@ 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); - 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); @@ -115,13 +148,10 @@ 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); - 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); @@ -143,14 +173,11 @@ 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); 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/GlyphTests.cs b/tests/SixLabors.Fonts.Tests/GlyphTests.cs index 7f9e8a0bb..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)); @@ -133,7 +134,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_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/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/Tables/AdvancedTypographic/GPos/GPosTableTests.cs b/tests/SixLabors.Fonts.Tests/Tables/AdvancedTypographic/GPos/GPosTableTests.cs index e9147f6a3..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, 263.999878f, 363, 322), - new(71, 324.999878f, 966, 1573), + new(246, 177, 363, 322), + new(71, 238, 966, 1573), }; // act @@ -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); } } diff --git a/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs b/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs index 00c318caa..97c9be22e 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); @@ -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 @@ -379,11 +379,115 @@ 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); + Font font2 = CreateFont(text, 16); + TextOptions options = new(font) + { + TextRuns = new List() + { + new TextRun() { Start = 9, End = 23, Font = font2 }, + new TextRun() { Start = 35, End = 54, Font = font2 }, + new TextRun() { Start = 68, End = 70, Font = font2 }, + } + }; + + 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(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); + } + + [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(); 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); + } } } 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)); + } + } +} 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)); diff --git a/tests/SixLabors.Fonts.Tests/WriterExtensions.cs b/tests/SixLabors.Fonts.Tests/WriterExtensions.cs index e15d2135b..224e7df8c 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,52 @@ 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)