Skip to content

Commit 9d266ac

Browse files
Correctly handle vertical glyph decorations
1 parent a2b5d0e commit 9d266ac

6 files changed

Lines changed: 142 additions & 25 deletions

File tree

src/ImageSharp.Drawing/ImageSharp.Drawing.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
<None Include="..\..\shared-infrastructure\branding\icons\imagesharp.drawing\sixlabors.imagesharp.drawing.128.png" Pack="true" PackagePath="" />
1919
</ItemGroup>
2020
<ItemGroup>
21-
<PackageReference Include="SixLabors.Fonts" Version="1.0.0-beta19.10" />
21+
<PackageReference Include="SixLabors.Fonts" Version="1.0.0-beta19.12" />
2222
<PackageReference Include="SixLabors.ImageSharp" Version="2.1.3" />
2323
</ItemGroup>
2424
<Import Project="..\..\shared-infrastructure\src\SharedInfrastructure\SharedInfrastructure.projitems" Label="Shared" />

src/ImageSharp.Drawing/Processing/Processors/Text/RichTextGlyphRenderer.cs

Lines changed: 39 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ internal sealed class RichTextGlyphRenderer : BaseGlyphBuilder, IColorGlyphRende
2424
private const byte RenderOrderOutline = 1;
2525
private const byte RenderOrderDecoration = 2;
2626

27-
private readonly RichTextOptions textOptions;
2827
private readonly DrawingOptions drawingOptions;
2928
private readonly MemoryAllocator memoryAllocator;
3029
private readonly Pen defaultPen;
@@ -40,7 +39,7 @@ internal sealed class RichTextGlyphRenderer : BaseGlyphBuilder, IColorGlyphRende
4039
private TextDecorationDetails? currentUnderline;
4140
private TextDecorationDetails? currentStrikout;
4241
private TextDecorationDetails? currentOverline;
43-
private bool currentDecorationsRotated;
42+
private bool currentDecorationRotated;
4443

4544
// Just enough accuracy to allow for 1/8 px differences which later are accumulated while rendering,
4645
// but do not grow into full px offsets.
@@ -61,7 +60,6 @@ public RichTextGlyphRenderer(
6160
Brush brush)
6261
: base(drawingOptions.Transform)
6362
{
64-
this.textOptions = textOptions;
6563
this.drawingOptions = drawingOptions;
6664
this.memoryAllocator = memoryAllocator;
6765
this.defaultPen = pen;
@@ -102,7 +100,7 @@ protected override void BeginText(in FontRectangle bounds)
102100
protected override void BeginGlyph(in FontRectangle bounds, in GlyphRendererParameters parameters)
103101
{
104102
this.currentColor = null;
105-
this.currentDecorationsRotated = parameters.LayoutMode.IsVertical() || parameters.LayoutMode.IsVerticalMixed();
103+
this.currentDecorationRotated = parameters.LayoutMode.IsVertical() || parameters.LayoutMode.IsVerticalMixed();
106104
this.currentTextRun = parameters.TextRun;
107105
if (parameters.TextRun is RichTextRun drawingRun)
108106
{
@@ -225,19 +223,14 @@ public override void SetDecoration(TextDecorations textDecorations, Vector2 star
225223
{
226224
thickness = pen.StrokeWidth;
227225
}
228-
229-
// Center the line at the given position.
230-
bool rotated = this.currentDecorationsRotated;
231-
Vector2 pad = rotated ? new(thickness * .5F, 0) : new(0, thickness * .5F);
232-
Vector2 a = start - pad;
233-
Vector2 b = start + pad;
234-
Vector2 d = end - pad;
235-
thickness = rotated ? b.X - a.X : b.Y - a.Y;
236-
237-
pen ??= new SolidPen(this.currentBrush ?? this.defaultBrush, thickness);
226+
else
227+
{
228+
pen = new SolidPen(this.currentBrush ?? this.defaultBrush, thickness);
229+
}
238230

239231
// Drawing is always centered around the point so we need to offset by half.
240232
Vector2 offset = Vector2.Zero;
233+
bool rotated = this.currentDecorationRotated;
241234
if (textDecorations == TextDecorations.Overline)
242235
{
243236
// CSS overline is drawn above the position, so we need to move it up.
@@ -249,8 +242,7 @@ public override void SetDecoration(TextDecorations textDecorations, Vector2 star
249242
offset = rotated ? new(-(thickness * .5F), 0) : new(0, thickness * .5F);
250243
}
251244

252-
// Clamp the line to whole pixels
253-
this.AppendDecoration(ref targetDecoration, ClampToPixel(a + offset), ClampToPixel(d + offset), pen, (float)Math.Truncate(thickness));
245+
this.AppendDecoration(ref targetDecoration, start + offset, end + offset, pen, thickness, rotated);
254246
}
255247

256248
protected override void EndGlyph()
@@ -425,24 +417,47 @@ private void FinalizeDecoration(ref TextDecorationDetails? decoration)
425417
}
426418
}
427419

428-
private void AppendDecoration(ref TextDecorationDetails? decoration, Vector2 start, Vector2 end, Pen pen, float thickness)
420+
private void AppendDecoration(
421+
ref TextDecorationDetails? decoration,
422+
Vector2 start,
423+
Vector2 end,
424+
Pen pen,
425+
float thickness,
426+
bool rotated)
429427
{
430428
if (decoration != null)
431429
{
432430
// TODO: This only works well if we are not trying to follow a path.
433431
if (this.path is null)
434432
{
435433
// Let's try and expand it first.
436-
if (thickness == decoration.Value.Thickness
437-
&& decoration.Value.End.Y == start.Y
438-
&& (decoration.Value.End.X + 1) >= start.X
434+
if (rotated)
435+
{
436+
if (thickness == decoration.Value.Thickness
437+
&& decoration.Value.End.Y + 1 >= start.Y
438+
&& decoration.Value.End.X == start.X
439439
&& decoration.Value.Pen.Equals(pen))
440+
{
441+
// Expand the line
442+
start = decoration.Value.Start;
443+
444+
// If this is null finalize does nothing.
445+
decoration = null;
446+
}
447+
}
448+
else
440449
{
441-
// Expand the line
442-
start = decoration.Value.Start;
450+
if (thickness == decoration.Value.Thickness
451+
&& decoration.Value.End.Y == start.Y
452+
&& decoration.Value.End.X + 1 >= start.X
453+
&& decoration.Value.Pen.Equals(pen))
454+
{
455+
// Expand the line
456+
start = decoration.Value.Start;
443457

444-
// If this is null finalize does nothing.
445-
decoration = null;
458+
// If this is null finalize does nothing.
459+
decoration = null;
460+
}
446461
}
447462
}
448463
}

tests/ImageSharp.Drawing.Tests/Drawing/Text/DrawTextOnImageTests.cs

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System;
55
using System.Collections.Generic;
6+
using System.IO;
67
using System.Linq;
78
using System.Numerics;
89
using System.Text;
@@ -785,6 +786,102 @@ public void PathAndTextDrawingMatch<TPixel>(TestImageProvider<TPixel> provider)
785786
});
786787
}
787788

789+
[Theory]
790+
[WithBlankImage(500, 400, PixelTypes.Rgba32)]
791+
public void CanFillTextVertical<TPixel>(TestImageProvider<TPixel> provider)
792+
where TPixel : unmanaged, IPixel<TPixel>
793+
{
794+
Font font = CreateFont(TestFonts.OpenSans, 36);
795+
Font fallback = CreateFont(TestFonts.NotoSansKRRegular, 36);
796+
797+
const string text = "한국어 hangugeo 한국어 hangugeo 한국어 hangugeo 한국어 hangugeo 한국어 hangugeo";
798+
RichTextOptions textOptions = new(font)
799+
{
800+
Origin = new(0, 0),
801+
FallbackFontFamilies = new[] { fallback.Family },
802+
WrappingLength = 300,
803+
LayoutMode = LayoutMode.VerticalLeftRight,
804+
TextRuns = new[] { new RichTextRun() { Start = 0, End = text.GetGraphemeCount(), TextDecorations = TextDecorations.Underline | TextDecorations.Strikeout | TextDecorations.Overline } }
805+
};
806+
807+
IPathCollection glyphs = TextBuilder.GenerateGlyphs(text, textOptions);
808+
809+
DrawingOptions options = new() { ShapeOptions = new() { IntersectionRule = IntersectionRule.Nonzero } };
810+
811+
provider.RunValidatingProcessorTest(
812+
c => c.Fill(Color.White).Fill(options, Color.Black, glyphs),
813+
comparer: ImageComparer.TolerantPercentage(0.002f));
814+
}
815+
816+
[Theory]
817+
[WithBlankImage(500, 400, PixelTypes.Rgba32)]
818+
public void CanFillTextVerticalMixed<TPixel>(TestImageProvider<TPixel> provider)
819+
where TPixel : unmanaged, IPixel<TPixel>
820+
{
821+
Font font = CreateFont(TestFonts.OpenSans, 36);
822+
Font fallback = CreateFont(TestFonts.NotoSansKRRegular, 36);
823+
824+
const string text = "한국어 hangugeo 한국어 hangugeo 한국어 hangugeo 한국어 hangugeo 한국어 hangugeo";
825+
RichTextOptions textOptions = new(font)
826+
{
827+
FallbackFontFamilies = new[] { fallback.Family },
828+
WrappingLength = 400,
829+
LayoutMode = LayoutMode.VerticalMixedLeftRight,
830+
TextRuns = new[] { new RichTextRun() { Start = 0, End = text.GetGraphemeCount(), TextDecorations = TextDecorations.Underline | TextDecorations.Strikeout | TextDecorations.Overline } }
831+
};
832+
833+
IPathCollection glyphs = TextBuilder.GenerateGlyphs(text, textOptions);
834+
DrawingOptions options = new() { ShapeOptions = new() { IntersectionRule = IntersectionRule.Nonzero } };
835+
836+
provider.RunValidatingProcessorTest(
837+
c => c.Fill(Color.White).Fill(options, Color.Black, glyphs),
838+
comparer: ImageComparer.TolerantPercentage(0.002f));
839+
}
840+
841+
[Theory]
842+
[WithBlankImage(500, 400, PixelTypes.Rgba32)]
843+
public void CanDrawTextVertical<TPixel>(TestImageProvider<TPixel> provider)
844+
where TPixel : unmanaged, IPixel<TPixel>
845+
{
846+
Font font = CreateFont(TestFonts.OpenSans, 36);
847+
Font fallback = CreateFont(TestFonts.NotoSansKRRegular, 36);
848+
849+
const string text = "한국어 hangugeo 한국어 hangugeo 한국어 hangugeo 한국어 hangugeo 한국어 hangugeo";
850+
RichTextOptions textOptions = new(font)
851+
{
852+
FallbackFontFamilies = new[] { fallback.Family },
853+
WrappingLength = 400,
854+
LayoutMode = LayoutMode.VerticalLeftRight,
855+
TextRuns = new[] { new RichTextRun() { Start = 0, End = text.GetGraphemeCount(), TextDecorations = TextDecorations.Underline | TextDecorations.Strikeout | TextDecorations.Overline } }
856+
};
857+
858+
provider.RunValidatingProcessorTest(
859+
c => c.Fill(Color.White).DrawText(textOptions, text, Brushes.Solid(Color.Black)),
860+
comparer: ImageComparer.TolerantPercentage(0.002f));
861+
}
862+
863+
[Theory]
864+
[WithBlankImage(500, 400, PixelTypes.Rgba32)]
865+
public void CanDrawTextVerticalMixed<TPixel>(TestImageProvider<TPixel> provider)
866+
where TPixel : unmanaged, IPixel<TPixel>
867+
{
868+
Font font = CreateFont(TestFonts.OpenSans, 36);
869+
Font fallback = CreateFont(TestFonts.NotoSansKRRegular, 36);
870+
871+
const string text = "한국어 hangugeo 한국어 hangugeo 한국어 hangugeo 한국어 hangugeo 한국어 hangugeo";
872+
RichTextOptions textOptions = new(font)
873+
{
874+
FallbackFontFamilies = new[] { fallback.Family },
875+
WrappingLength = 400,
876+
LayoutMode = LayoutMode.VerticalMixedLeftRight,
877+
TextRuns = new[] { new RichTextRun() { Start = 0, End = text.GetGraphemeCount(), TextDecorations = TextDecorations.Underline | TextDecorations.Strikeout | TextDecorations.Overline } }
878+
};
879+
880+
provider.RunValidatingProcessorTest(
881+
c => c.Fill(Color.White).DrawText(textOptions, text, Brushes.Solid(Color.Black)),
882+
comparer: ImageComparer.TolerantPercentage(0.002f));
883+
}
884+
788885
private static string Repeat(string str, int times) => string.Concat(Enumerable.Repeat(str, times));
789886

790887
private static string ToTestOutputDisplayText(string text)

tests/ImageSharp.Drawing.Tests/ImageSharp.Drawing.Tests.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@
2626
<None Update="TestFonts\*.woff">
2727
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
2828
</None>
29+
<None Update="TestFonts\*.otf">
30+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
31+
</None>
2932
<None Update="xunit.runner.json">
3033
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
3134
</None>

tests/ImageSharp.Drawing.Tests/TestFonts.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,7 @@ public static class TestFonts
2222
public const string MeQuranVolyNewmet = "me_quran_volt_newmet.ttf";
2323

2424
public const string NettoOffc = "NettoOffc.ttf";
25+
26+
public const string NotoSansKRRegular = "NotoSansKR-Regular.otf";
2527
}
2628
}
4.52 MB
Binary file not shown.

0 commit comments

Comments
 (0)