From 7a533700d0bcb54be20ec448e5b7802ad6ac677b Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Thu, 9 Jun 2022 12:24:39 +0200 Subject: [PATCH] Implement GlyphRun.BuildGeometry --- samples/RenderDemo/Pages/GlyphRunPage.xaml | 12 +- samples/RenderDemo/Pages/GlyphRunPage.xaml.cs | 113 +++++++++++++----- src/Avalonia.Base/Media/GlyphRun.cs | 19 +++ src/Avalonia.Base/Media/PlatformGeometry.cs | 24 ++++ .../Platform/IPlatformRenderInterface.cs | 8 ++ .../Avalonia.Skia/PlatformRenderInterface.cs | 32 +++++ .../Avalonia.Direct2D1/Direct2D1Platform.cs | 28 +++++ .../Media/GlyphRunTests.cs | 66 ++++++++++ ...ould_Render_GlyphRun_Geometry.expected.png | Bin 0 -> 1476 bytes ...ould_Render_GlyphRun_Geometry.expected.png | Bin 0 -> 1170 bytes 10 files changed, 269 insertions(+), 33 deletions(-) create mode 100644 src/Avalonia.Base/Media/PlatformGeometry.cs create mode 100644 tests/Avalonia.RenderTests/Media/GlyphRunTests.cs create mode 100644 tests/TestFiles/Direct2D1/Media/GlyphRun/Should_Render_GlyphRun_Geometry.expected.png create mode 100644 tests/TestFiles/Skia/Media/GlyphRun/Should_Render_GlyphRun_Geometry.expected.png diff --git a/samples/RenderDemo/Pages/GlyphRunPage.xaml b/samples/RenderDemo/Pages/GlyphRunPage.xaml index c2914e8847..7db58e5286 100644 --- a/samples/RenderDemo/Pages/GlyphRunPage.xaml +++ b/samples/RenderDemo/Pages/GlyphRunPage.xaml @@ -2,13 +2,13 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:local="clr-namespace:RenderDemo.Pages" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="RenderDemo.Pages.GlyphRunPage"> - - - - + + + diff --git a/samples/RenderDemo/Pages/GlyphRunPage.xaml.cs b/samples/RenderDemo/Pages/GlyphRunPage.xaml.cs index 7f85606957..674ed8e61f 100644 --- a/samples/RenderDemo/Pages/GlyphRunPage.xaml.cs +++ b/samples/RenderDemo/Pages/GlyphRunPage.xaml.cs @@ -9,14 +9,6 @@ namespace RenderDemo.Pages { public class GlyphRunPage : UserControl { - private Image _imageControl; - private GlyphTypeface _glyphTypeface = Typeface.Default.GlyphTypeface; - private readonly Random _rand = new Random(); - private ushort[] _glyphIndices = new ushort[1]; - private char[] _characters = new char[1]; - private float _fontSize = 20; - private int _direction = 10; - public GlyphRunPage() { this.InitializeComponent(); @@ -25,19 +17,43 @@ namespace RenderDemo.Pages private void InitializeComponent() { AvaloniaXamlLoader.Load(this); + } + } + + public class GlyphRunControl : Control + { + private GlyphTypeface _glyphTypeface = Typeface.Default.GlyphTypeface; + private readonly Random _rand = new Random(); + private ushort[] _glyphIndices = new ushort[1]; + private char[] _characters = new char[1]; + private float _fontSize = 20; + private int _direction = 10; - _imageControl = this.FindControl("imageControl"); - _imageControl.Source = new DrawingImage(); + private DispatcherTimer _timer; - DispatcherTimer.Run(() => + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + _timer = new DispatcherTimer + { + Interval = TimeSpan.FromSeconds(1) + }; + + _timer.Tick += (s,e) => { - UpdateGlyphRun(); + InvalidateVisual(); + }; + + _timer.Start(); + } + + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + _timer.Stop(); - return true; - }, TimeSpan.FromSeconds(1)); + _timer = null; } - private void UpdateGlyphRun() + public override void Render(DrawingContext context) { var c = (char)_rand.Next(65, 90); @@ -57,27 +73,70 @@ namespace RenderDemo.Pages _characters[0] = c; - var scale = (double)_fontSize / _glyphTypeface.DesignEmHeight; + var glyphRun = new GlyphRun(_glyphTypeface, _fontSize, _characters, _glyphIndices); - var drawingGroup = new DrawingGroup(); + context.DrawGlyphRun(Brushes.Black, glyphRun); + } + } - var glyphRunDrawing = new GlyphRunDrawing + public class GlyphRunGeometryControl : Control + { + private GlyphTypeface _glyphTypeface = Typeface.Default.GlyphTypeface; + private readonly Random _rand = new Random(); + private ushort[] _glyphIndices = new ushort[1]; + private char[] _characters = new char[1]; + private float _fontSize = 20; + private int _direction = 10; + + private DispatcherTimer _timer; + + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + _timer = new DispatcherTimer { - Foreground = Brushes.Black, - GlyphRun = new GlyphRun(_glyphTypeface, _fontSize, _characters, _glyphIndices) + Interval = TimeSpan.FromSeconds(1) }; - drawingGroup.Children.Add(glyphRunDrawing); - - var geometryDrawing = new GeometryDrawing + _timer.Tick += (s, e) => { - Pen = new Pen(Brushes.Black), - Geometry = new RectangleGeometry { Rect = new Rect(glyphRunDrawing.GlyphRun.Size) } + InvalidateVisual(); }; - drawingGroup.Children.Add(geometryDrawing); + _timer.Start(); + } + + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + _timer.Stop(); + + _timer = null; + } + + public override void Render(DrawingContext context) + { + var c = (char)_rand.Next(65, 90); + + if (_fontSize + _direction > 200) + { + _direction = -10; + } + + if (_fontSize + _direction < 20) + { + _direction = 10; + } + + _fontSize += _direction; + + _glyphIndices[0] = _glyphTypeface.GetGlyph(c); + + _characters[0] = c; + + var glyphRun = new GlyphRun(_glyphTypeface, _fontSize, _characters, _glyphIndices); + + var geometry = glyphRun.BuildGeometry(); - (_imageControl.Source as DrawingImage).Drawing = drawingGroup; + context.DrawGeometry(Brushes.Green, null, geometry); } } } diff --git a/src/Avalonia.Base/Media/GlyphRun.cs b/src/Avalonia.Base/Media/GlyphRun.cs index 22be8d8865..25c35a28e5 100644 --- a/src/Avalonia.Base/Media/GlyphRun.cs +++ b/src/Avalonia.Base/Media/GlyphRun.cs @@ -194,6 +194,25 @@ namespace Avalonia.Media } } + /// + /// Obtains geometry for the glyph run. + /// + /// The geometry returned contains the combined geometry of all glyphs in the glyph run. + public Geometry BuildGeometry() + { + var platformRenderInterface = AvaloniaLocator.Current.GetRequiredService(); + + var geometryImpl = platformRenderInterface.BuildGlyphRunGeometry(this, out var scale); + + var geometry = new PlatformGeometry(geometryImpl); + + var transform = new MatrixTransform(Matrix.CreateTranslation(geometry.Bounds.Left, -geometry.Bounds.Top) * scale); + + geometry.Transform = transform; + + return geometry; + } + /// /// Retrieves the offset from the leading edge of the /// to the leading or trailing edge of a caret stop containing the specified character hit. diff --git a/src/Avalonia.Base/Media/PlatformGeometry.cs b/src/Avalonia.Base/Media/PlatformGeometry.cs new file mode 100644 index 0000000000..f25a14540f --- /dev/null +++ b/src/Avalonia.Base/Media/PlatformGeometry.cs @@ -0,0 +1,24 @@ +using Avalonia.Platform; + +namespace Avalonia.Media +{ + internal class PlatformGeometry : Geometry + { + private readonly IGeometryImpl _geometryImpl; + + public PlatformGeometry(IGeometryImpl geometryImpl) + { + _geometryImpl = geometryImpl; + } + + public override Geometry Clone() + { + return new PlatformGeometry(_geometryImpl); + } + + protected override IGeometryImpl? CreateDefiningGeometry() + { + return _geometryImpl; + } + } +} diff --git a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs index 0eeefddf0b..bfa9e70fce 100644 --- a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs +++ b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs @@ -58,6 +58,14 @@ namespace Avalonia.Platform /// A combined geometry. IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2); + /// + /// Created a geometry implementation for the glyph run. + /// + /// The glyph run to build a geometry from. + /// The scaling of the produces geometry. + /// The geometry returned contains the combined geometry of all glyphs in the glyph run. + IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun, out Matrix scale); + /// /// Creates a renderer. /// diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index d3c3585cd0..dc1b7785e2 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs @@ -62,6 +62,38 @@ namespace Avalonia.Skia return new CombinedGeometryImpl(combineMode, g1, g2); } + public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun, out Matrix scale) + { + if (glyphRun.GlyphTypeface.PlatformImpl is not GlyphTypefaceImpl glyphTypeface) + { + throw new InvalidOperationException("PlatformImpl can't be null."); + } + + var fontRenderingEmSize = (float)glyphRun.FontRenderingEmSize; + var glyphs = glyphRun.GlyphIndices.ToArray(); + var skFont = new SKFont(glyphTypeface.Typeface, fontRenderingEmSize) + { + Size = fontRenderingEmSize, + Edging = SKFontEdging.Antialias, + Hinting = SKFontHinting.None, + LinearMetrics = true + }; + + SKPath path = null; + var matrix = SKMatrix.Identity; + + skFont.GetGlyphPaths(glyphs, (p, m) => + { + matrix = m; + + path = p; + }); + + scale = Matrix.CreateScale(matrix.ScaleX, matrix.ScaleY); + + return new StreamGeometryImpl(path); + } + /// public IBitmapImpl LoadBitmap(string fileName) { diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs index d9e992bb80..04025f92e4 100644 --- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs +++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs @@ -159,6 +159,34 @@ namespace Avalonia.Direct2D1 public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children) => new GeometryGroupImpl(fillRule, children); public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2) => new CombinedGeometryImpl(combineMode, g1, g2); + public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun, out Matrix scale) + { + if (glyphRun.GlyphTypeface.PlatformImpl is not GlyphTypefaceImpl glyphTypeface) + { + throw new InvalidOperationException("PlatformImpl can't be null."); + } + + var pathGeometry = new SharpDX.Direct2D1.PathGeometry(Direct2D1Factory); + + using (var sink = pathGeometry.Open()) + { + var glyphs = new short[glyphRun.GlyphIndices.Count]; + + for (int i = 0; i < glyphRun.GlyphIndices.Count; i++) + { + glyphs[i] = (short)glyphRun.GlyphIndices[i]; + } + + glyphTypeface.FontFace.GetGlyphRunOutline((float)glyphRun.FontRenderingEmSize, glyphs, null, null, false, !glyphRun.IsLeftToRight, sink); + + sink.Close(); + } + + scale = Matrix.Identity; + + return new StreamGeometryImpl(pathGeometry); + } + /// public IBitmapImpl LoadBitmap(string fileName) { diff --git a/tests/Avalonia.RenderTests/Media/GlyphRunTests.cs b/tests/Avalonia.RenderTests/Media/GlyphRunTests.cs new file mode 100644 index 0000000000..734e4d5012 --- /dev/null +++ b/tests/Avalonia.RenderTests/Media/GlyphRunTests.cs @@ -0,0 +1,66 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Avalonia.Controls; +using Avalonia.Controls.Shapes; +using Avalonia.Media; +using Avalonia.Media.Imaging; +using Xunit; + +#if AVALONIA_SKIA +namespace Avalonia.Skia.RenderTests +#else +namespace Avalonia.Direct2D1.RenderTests.Media +#endif +{ + public class GlyphRunTests : TestBase + { + public GlyphRunTests() + : base(@"Media\GlyphRun") + { + } + + [Fact] + public async Task Should_Render_GlyphRun_Geometry() + { + Decorator target = new Decorator + { + Padding = new Thickness(8), + Width = 80, + Height = 90, + Child = new GlyphRunGeometryControl() + }; + + await RenderToFile(target); + + CompareImages(); + } + + public class GlyphRunGeometryControl : Control + { + private readonly Geometry _geometry; + + public GlyphRunGeometryControl() + { + var glyphTypeface = Typeface.Default.GlyphTypeface; + + var glyphIndices = new[] { glyphTypeface.GetGlyph('A') }; + + var characters = new[] { 'A' }; + + var glyphRun = new GlyphRun(glyphTypeface, 100, characters, glyphIndices); + + _geometry = glyphRun.BuildGeometry(); + } + + protected override Size MeasureOverride(Size availableSize) + { + return _geometry.Bounds.Size; + } + + public override void Render(DrawingContext context) + { + context.DrawGeometry(Brushes.Green, null, _geometry); + } + } + } +} diff --git a/tests/TestFiles/Direct2D1/Media/GlyphRun/Should_Render_GlyphRun_Geometry.expected.png b/tests/TestFiles/Direct2D1/Media/GlyphRun/Should_Render_GlyphRun_Geometry.expected.png new file mode 100644 index 0000000000000000000000000000000000000000..0b40e667a408be1bda12d7749d0496a85a20ce3e GIT binary patch literal 1476 zcmV;#1v~nQP)Px#1ZP1_K>z@;j|==^1pojAZb?KzRCr$PoWE;SK@i9H#KOYD!a^ZJiWDg zks?A0g%l|w1Po~5fAQ}A0!vF3B18yiks^hKMT8V7QYr+rNMVt}!oos0XKwb)C3!nD z@4fvMvLCqjb|K*1cW2+ue0QIwP$(1%g+iflCF$9FK2&BJzSVczbFpLk=CN%j<(j?c zL(e?3EskXn`i^ZUB`*Oha~-Z5)M3&7U2t-Xz$N%hQB$C2p4wJY@)A%teaFl~#6Ga? zWaJ}YWp2au5p|R_6FdkxMc^8IZctOrDhPkmwv&*LfbF6Q#Y5qfk%xelnTP8fb!s`X z_lqQ+#(4A4mE38fB~4b?dYTzuo^x0)~Qp=z8RWtwp~xcD9j-OSKxDtnl%i~ z7r5?I$H_Vj1SZ$fNiU!rJ-cRL4&i=G-CD4$!-0rSS^?2Gpz6cLA&B?Gwm;f?J&7ul z)Dg(nvqSUFwy^!ywp^gWpeqACl2$-IYW~7!i<-c|9KiJ}b!x$9Ah#F=A)SELXzJn= zcJUcRJNw*|qAO!h)yb@o)YGfLqdEnw0 zqR%}|2af4INyKR!UqE@KlokppLJ>3=?%Q@y;tEK{0q@_*&p3=ij8Lw$ix!2kBM>cn zwC-ITfH+*TJjok-cd-Ej7TjQnE1;bFNmKI_`EPNcf*TC+1f)8ZGY+tHlb?TMC{lTw zD?()wGXl{#;HK0Lh;m}vI^-x^vI7^4cmn#IzNetsP@ixU!A~&65s>Or&N%o5A8y~H z9*Wpq>Np5IjpGO?{|I;KCLyOi({=!Z5MDqs4pyn-K$H8q!N}#GodKgTMg&SH4vrxD z_H4U3gLB4fJ$Qn_y>Zon@B(%-4s^&7ELT7+2`eDgqYKn=qR;(Y$aa6QX&hESxgOnv zAMZZ4(F8dP=k0)cC7gho)22SA?d$5BIPg1t$4GX+W?&l;Dgtp&f-?><@oiqZNoeeT&nFnd z2q-_SH{r)(0_-1MWIwm}&iEIQPR`Fz$3d9sOGLK&{lDtfH?QHE#Y-?OL-Wvz-3h-D zNE_O`agYn1azfuz80*cXuN^dC1qQsc!QfZGfVNQpyqsY0Cm@}amyCnF@UWQp6Yz3H z-xxF)xc5^;{0K_iO!S?DILegOV9~h5E&%A^yw^*FOce@AQ zuGOB59|8L#@wg%#zvSIu)dxr4yn^cs>NpshC-B``IYZs2Fui&&y{5XAL_9>ZVbx(8 z`*;RQ#sU6__qG+E!RYvhbYKZjFmU_UPPz*yk6yQ3Hy`M>O{+AauE+ol%?;}_yj{c0jrVYEd8G?_^#Ve+s=ak zIbk~iFU#U_9$ij22LZyp$8v_|0_uzdoG$ze(y8#gn6!yNT=^g<6^kNK3qQG(tYNoEnddNz(|FUR6NSIWKxNIpHD%jA$@4 z6Y#Q6-!ZzJP!)xG0m(R!4k)H~UbN`aEb)Ciy@0x5S`erhdK?hyU`shen>jF$BlgVTpXTyZ)boGd%}JK za^v9c&-6VJNUzQJggrBmWlIo$_Lk-bLmvT|ad5y3{0EDM$0Op#I)y7ASU$ZnFr9s? zWTxFD+JMFeLoWeYtqO@Yww-BeFtis?F%Ep<=oS~2Tk@17xYSQfoM>j?w?bQv2mICU=fsXP!Ri|+q0X002J$^RqAKfNQMqEfH~r1tMAr$Xt#m_)b$iPSMo(qQOvjv|K)g`xyr+ zL5Ojsom40Jdz7Ks{lx}@g@A1O?hS4GQi2duv?p99U~l6t7_!fO`^Ebc3N>K>nI45MP#gEPjS9L5Oj=oi52h zqFoD?42E0*mFXKY`|N{OryUE>1=Ng#{U?~duPAkzCQMESDm5Ww^w$?{PdLOMd7Mlp klgVT