77namespace OxyPlot . SkiaSharp
88{
99 using global ::SkiaSharp ;
10+ using global ::SkiaSharp . HarfBuzz ;
1011 using System ;
1112 using System . Collections . Generic ;
1213 using System . Linq ;
@@ -16,6 +17,7 @@ namespace OxyPlot.SkiaSharp
1617 /// </summary>
1718 public class SkiaRenderContext : IRenderContext , IDisposable
1819 {
20+ private readonly Dictionary < FontDescriptor , SKShaper > shaperCache = new Dictionary < FontDescriptor , SKShaper > ( ) ;
1921 private readonly Dictionary < FontDescriptor , SKTypeface > typefaceCache = new Dictionary < FontDescriptor , SKTypeface > ( ) ;
2022 private SKPaint paint = new SKPaint ( ) ;
2123 private SKPath path = new SKPath ( ) ;
@@ -33,6 +35,11 @@ public class SkiaRenderContext : IRenderContext, IDisposable
3335 /// </summary>
3436 public SKCanvas SkCanvas { get ; set ; }
3537
38+ /// <summary>
39+ /// Gets or sets a value indicating whether text shaping should be used when rendering text.
40+ /// </summary>
41+ public bool UseTextShaping { get ; set ; } = true ;
42+
3643 /// <inheritdoc/>
3744 public void CleanUp ( )
3845 {
@@ -341,18 +348,11 @@ public void DrawText(
341348 return ;
342349 }
343350
344- var paint = this . GetTextPaint ( fontFamily , fontSize , fontWeight ) ;
351+ var paint = this . GetTextPaint ( fontFamily , fontSize , fontWeight , out var shaper ) ;
345352 paint . Color = fill . ToSKColor ( ) ;
346353
347- var width = paint . MeasureText ( text ) ;
348-
349- var deltaX = horizontalAlignment switch
350- {
351- HorizontalAlignment . Left => 0 ,
352- HorizontalAlignment . Center => - width / 2 ,
353- HorizontalAlignment . Right => - width ,
354- _ => throw new ArgumentOutOfRangeException ( nameof ( horizontalAlignment ) )
355- } ;
354+ var x = this . Convert ( p . X ) ;
355+ var y = this . Convert ( p . Y ) ;
356356
357357 var metrics = paint . FontMetrics ;
358358 var deltaY = verticalAlignment switch
@@ -363,13 +363,35 @@ public void DrawText(
363363 _ => throw new ArgumentOutOfRangeException ( nameof ( verticalAlignment ) )
364364 } ;
365365
366- var x = this . Convert ( p . X ) ;
367- var y = this . Convert ( p . Y ) ;
368- using ( new SKAutoCanvasRestore ( this . SkCanvas ) )
366+ using var _ = new SKAutoCanvasRestore ( this . SkCanvas ) ;
367+ this . SkCanvas . Translate ( x , y ) ;
368+ this . SkCanvas . RotateDegrees ( ( float ) rotation ) ;
369+
370+ if ( this . UseTextShaping )
371+ {
372+ var width = this . MeasureText ( text , shaper , paint ) ;
373+ var deltaX = horizontalAlignment switch
374+ {
375+ HorizontalAlignment . Left => 0 ,
376+ HorizontalAlignment . Center => - width / 2 ,
377+ HorizontalAlignment . Right => - width ,
378+ _ => throw new ArgumentOutOfRangeException ( nameof ( horizontalAlignment ) )
379+ } ;
380+
381+ this . paint . TextAlign = SKTextAlign . Left ;
382+ this . SkCanvas . DrawShapedText ( shaper , text , deltaX , deltaY , paint ) ;
383+ }
384+ else
369385 {
370- this . SkCanvas . Translate ( x , y ) ;
371- this . SkCanvas . RotateDegrees ( ( float ) rotation ) ;
372- this . SkCanvas . DrawText ( text , new SKPoint ( deltaX , deltaY ) , paint ) ;
386+ paint . TextAlign = horizontalAlignment switch
387+ {
388+ HorizontalAlignment . Left => SKTextAlign . Left ,
389+ HorizontalAlignment . Center => SKTextAlign . Center ,
390+ HorizontalAlignment . Right => SKTextAlign . Right ,
391+ _ => throw new ArgumentOutOfRangeException ( nameof ( horizontalAlignment ) )
392+ } ;
393+
394+ this . SkCanvas . DrawText ( text , 0 , deltaY , paint ) ;
373395 }
374396 }
375397
@@ -381,8 +403,8 @@ public OxySize MeasureText(string text, string fontFamily = null, double fontSiz
381403 return new OxySize ( 0 , 0 ) ;
382404 }
383405
384- var paint = this . GetTextPaint ( fontFamily , fontSize , fontWeight ) ;
385- var width = paint . MeasureText ( text ) ;
406+ var paint = this . GetTextPaint ( fontFamily , fontSize , fontWeight , out var shaper ) ;
407+ var width = this . MeasureText ( text , shaper , paint ) ;
386408 var height = paint . GetFontMetrics ( out _ ) ;
387409 return new OxySize ( this . ConvertBack ( width ) , this . ConvertBack ( height ) ) ;
388410 }
@@ -434,6 +456,13 @@ protected virtual void Dispose(bool disposing)
434456 }
435457
436458 this . typefaceCache . Clear ( ) ;
459+
460+ foreach ( var shaper in this . shaperCache . Values )
461+ {
462+ shaper . Dispose ( ) ;
463+ }
464+
465+ this . shaperCache . Clear ( ) ;
437466 }
438467
439468 /// <summary>
@@ -506,23 +535,6 @@ private float Convert(double value)
506535 return ( float ) value * this . DpiScale ;
507536 }
508537
509- /// <summary>
510- /// Converts <see cref="double"/> dash array to a <see cref="float"/> array, taking into account DPI scaling.
511- /// </summary>
512- /// <param name="values">The array of values.</param>
513- /// <param name="strokeThickness">The stroke thickness.</param>
514- /// <returns>The array of converted values.</returns>
515- private float [ ] ConvertDashArray ( double [ ] values , float strokeThickness )
516- {
517- var ret = new float [ values . Length ] ;
518- for ( var i = 0 ; i < values . Length ; i ++ )
519- {
520- ret [ i ] = this . Convert ( values [ i ] ) * strokeThickness ;
521- }
522-
523- return ret ;
524- }
525-
526538 /// <summary>
527539 /// Converts <see cref="ScreenPoint"/> to a <see cref="SKPoint"/>, taking into account DPI scaling.
528540 /// </summary>
@@ -543,6 +555,23 @@ private double ConvertBack(float value)
543555 return value / this . DpiScale ;
544556 }
545557
558+ /// <summary>
559+ /// Converts <see cref="double"/> dash array to a <see cref="float"/> array, taking into account DPI scaling.
560+ /// </summary>
561+ /// <param name="values">The array of values.</param>
562+ /// <param name="strokeThickness">The stroke thickness.</param>
563+ /// <returns>The array of converted values.</returns>
564+ private float [ ] ConvertDashArray ( double [ ] values , float strokeThickness )
565+ {
566+ var ret = new float [ values . Length ] ;
567+ for ( var i = 0 ; i < values . Length ; i ++ )
568+ {
569+ ret [ i ] = this . Convert ( values [ i ] ) * strokeThickness ;
570+ }
571+
572+ return ret ;
573+ }
574+
546575 /// <summary>
547576 /// Converts a <see cref="OxyRect"/> to a <see cref="SKRect"/>, taking into account DPI scaling and snapping the corners to pixels.
548577 /// </summary>
@@ -786,8 +815,9 @@ private SKPaint GetStrokePaint(OxyColor strokeColor, double strokeThickness, Edg
786815 /// <param name="fontFamily">The font family.</param>
787816 /// <param name="fontSize">The font size.</param>
788817 /// <param name="fontWeight">The font weight.</param>
818+ /// <param name="shaper">The font shaper.</param>
789819 /// <returns>The paint.</returns>
790- private SKPaint GetTextPaint ( string fontFamily , double fontSize , double fontWeight )
820+ private SKPaint GetTextPaint ( string fontFamily , double fontSize , double fontWeight , out SKShaper shaper )
791821 {
792822 var fontDescriptor = new FontDescriptor ( fontFamily , fontWeight ) ;
793823 if ( ! this . typefaceCache . TryGetValue ( fontDescriptor , out var typeface ) )
@@ -796,16 +826,65 @@ private SKPaint GetTextPaint(string fontFamily, double fontSize, double fontWeig
796826 this . typefaceCache . Add ( fontDescriptor , typeface ) ;
797827 }
798828
829+ if ( this . UseTextShaping )
830+ {
831+ if ( ! this . shaperCache . TryGetValue ( fontDescriptor , out shaper ) )
832+ {
833+ shaper = new SKShaper ( typeface ) ;
834+ this . shaperCache . Add ( fontDescriptor , shaper ) ;
835+ }
836+ }
837+ else
838+ {
839+ shaper = null ;
840+ }
841+
799842 this . paint . Typeface = typeface ;
800843 this . paint . TextSize = this . Convert ( fontSize ) ;
801844 this . paint . IsAntialias = true ;
802845 this . paint . Style = SKPaintStyle . Fill ;
803846 this . paint . HintingLevel = this . RendersToScreen ? SKPaintHinting . Full : SKPaintHinting . NoHinting ;
804847 this . paint . SubpixelText = this . RendersToScreen ;
805- this . paint . TextAlign = SKTextAlign . Left ;
806848 return this . paint ;
807849 }
808850
851+ /// <summary>
852+ /// Measures text using the specified <see cref="SKShaper"/> and <see cref="SKPaint"/>.
853+ /// </summary>
854+ /// <param name="text">The text to measure.</param>
855+ /// <param name="shaper">The text shaper.</param>
856+ /// <param name="paint">The paint.</param>
857+ /// <returns>The width of the text when rendered using the specified shaper and paint.</returns>
858+ private float MeasureText ( string text , SKShaper shaper , SKPaint paint )
859+ {
860+ if ( ! this . UseTextShaping )
861+ {
862+ return paint . MeasureText ( text ) ;
863+ }
864+
865+ // we have to get a bit creative here as SKShaper does not offer a direct overload for this.
866+ // see also https://github.com/mono/SkiaSharp/blob/master/source/SkiaSharp.HarfBuzz/SkiaSharp.HarfBuzz.Shared/SKShaper.cs
867+ using var buffer = new HarfBuzzSharp . Buffer ( ) ;
868+ switch ( paint . TextEncoding )
869+ {
870+ case SKTextEncoding . Utf8 :
871+ buffer . AddUtf8 ( text ) ;
872+ break ;
873+ case SKTextEncoding . Utf16 :
874+ buffer . AddUtf16 ( text ) ;
875+ break ;
876+ case SKTextEncoding . Utf32 :
877+ buffer . AddUtf32 ( text ) ;
878+ break ;
879+ default :
880+ throw new NotSupportedException ( "TextEncoding is not supported." ) ;
881+ }
882+
883+ buffer . GuessSegmentProperties ( ) ;
884+ shaper . Shape ( buffer , paint ) ;
885+ return buffer . GlyphPositions . Sum ( gp => gp . XAdvance ) * paint . TextSize / 512 ;
886+ }
887+
809888 /// <summary>
810889 /// Gets a value indicating whether anti-aliasing should be used taking in account the specified edge rendering mode.
811890 /// </summary>
0 commit comments