// --------------------------------------------------------------------------------------------------------------------
//
// Copyright (c) 2020 OxyPlot contributors
//
// --------------------------------------------------------------------------------------------------------------------
namespace OxyPlot.SkiaSharp
{
using global::SkiaSharp;
using global::SkiaSharp.HarfBuzz;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
///
/// Implements based on SkiaSharp.
///
public class SkiaRenderContext : IRenderContext, IDisposable
{
private readonly Dictionary shaperCache = new Dictionary();
private readonly Dictionary typefaceCache = new Dictionary();
private SKPaint paint = new SKPaint();
private SKPath path = new SKPath();
private readonly Dictionary _fontWeights = new Dictionary()
{
[100] = "Thin",
[200] = "ExtraLight",
[300] = "Light",
[400] = "Regular",
[500] = "Medium",
[600] = "SemiBold",
[700] = "Bold",
[800] = "ExtraBold",
[900] = "Black"
};
///
/// Gets or sets the DPI scaling factor. A value of 1 corresponds to 96 DPI (dots per inch).
///
public float DpiScale { get; set; } = 1;
///
public bool RendersToScreen => this.RenderTarget == RenderTarget.Screen;
///
/// Gets or sets the render target.
///
public RenderTarget RenderTarget { get; set; } = RenderTarget.Screen;
///
/// Gets or sets the the renders to. This must be set before any draw calls.
///
public SKCanvas SkCanvas { get; set; }
///
/// Gets or sets a value indicating whether text shaping should be used when rendering text.
///
public bool UseTextShaping { get; set; } = true;
///
/// Gets or sets the Miter limit. This is the maximum ratio between Miter length and stroke thickness. When this ration is exceeded, the join falls back to a Bevel. The default value is 10.
///
public float MiterLimit { get; set; } = 10;
///
/// Gets a value indicating whether the context renders to pixels.
///
/// true if the context renders to pixels; otherwise, false.
private bool RendersToPixels => this.RenderTarget != RenderTarget.VectorGraphic;
///
public int ClipCount => this.SkCanvas?.SaveCount - 1 ?? 0;
///
public void CleanUp()
{
}
///
public void Dispose()
{
this.Dispose(true);
GC.SuppressFinalize(this);
}
///
public void DrawEllipse(OxyRect extents, OxyColor fill, OxyColor stroke, double thickness, EdgeRenderingMode edgeRenderingMode)
{
if (!fill.IsVisible() && !(stroke.IsVisible() || thickness <= 0))
{
return;
}
var actualRect = this.Convert(extents);
if (fill.IsVisible())
{
var paint = this.GetFillPaint(fill, edgeRenderingMode);
this.SkCanvas.DrawOval(actualRect, paint);
}
if (stroke.IsVisible() && thickness > 0)
{
var paint = this.GetStrokePaint(stroke, thickness, edgeRenderingMode);
this.SkCanvas.DrawOval(actualRect, paint);
}
}
///
public void DrawEllipses(IList extents, OxyColor fill, OxyColor stroke, double thickness, EdgeRenderingMode edgeRenderingMode)
{
if (!fill.IsVisible() && (!stroke.IsVisible() || thickness <= 0))
{
return;
}
var path = this.GetPath();
foreach (var extent in extents)
{
path.AddOval(this.Convert(extent));
}
if (fill.IsVisible())
{
var paint = this.GetFillPaint(fill, edgeRenderingMode);
this.SkCanvas.DrawPath(path, paint);
}
if (stroke.IsVisible() && thickness > 0)
{
var paint = this.GetStrokePaint(stroke, thickness, edgeRenderingMode);
this.SkCanvas.DrawPath(path, paint);
}
}
///
public void DrawImage(
OxyImage source,
double srcX,
double srcY,
double srcWidth,
double srcHeight,
double destX,
double destY,
double destWidth,
double destHeight,
double opacity,
bool interpolate)
{
if (source == null)
{
return;
}
var bytes = source.GetData();
var image = SKBitmap.Decode(bytes);
var src = new SKRect((float)srcX, (float)srcY, (float)(srcX + srcWidth), (float)(srcY + srcHeight));
var dest = new SKRect(this.Convert(destX), this.Convert(destY), this.Convert(destX + destWidth), this.Convert(destY + destHeight));
var paint = this.GetImagePaint(opacity, interpolate);
this.SkCanvas.DrawBitmap(image, src, dest, paint);
}
///
public void DrawLine(
IList points,
OxyColor stroke,
double thickness,
EdgeRenderingMode edgeRenderingMode,
double[] dashArray = null,
LineJoin lineJoin = LineJoin.Miter)
{
if (points.Count < 2 || !stroke.IsVisible() || thickness <= 0)
{
return;
}
var path = this.GetPath();
var paint = this.GetLinePaint(stroke, thickness, edgeRenderingMode, dashArray, lineJoin);
var actualPoints = this.GetActualPoints(points, thickness, edgeRenderingMode);
AddPoints(actualPoints, path);
this.SkCanvas.DrawPath(path, paint);
}
///
public void DrawLineSegments(
IList points,
OxyColor stroke,
double thickness,
EdgeRenderingMode edgeRenderingMode,
double[] dashArray = null,
LineJoin lineJoin = LineJoin.Miter)
{
if (points.Count < 2 || !stroke.IsVisible() || thickness <= 0)
{
return;
}
var paint = this.GetLinePaint(stroke, thickness, edgeRenderingMode, dashArray, lineJoin);
var skPoints = new SKPoint[points.Count];
switch (edgeRenderingMode)
{
case EdgeRenderingMode.Automatic when this.RendersToPixels:
case EdgeRenderingMode.Adaptive when this.RendersToPixels:
case EdgeRenderingMode.PreferSharpness when this.RendersToPixels:
var snapOffset = this.GetSnapOffset(thickness, edgeRenderingMode);
for (var i = 0; i < points.Count - 1; i += 2)
{
var p1 = points[i];
var p2 = points[i + 1];
if (RenderContextBase.IsStraightLine(p1, p2))
{
skPoints[i] = this.ConvertSnap(p1, snapOffset);
skPoints[i + 1] = this.ConvertSnap(p2, snapOffset);
}
else
{
skPoints[i] = this.Convert(p1);
skPoints[i + 1] = this.Convert(p2);
}
}
break;
default:
for (var i = 0; i < points.Count; i += 2)
{
skPoints[i] = this.Convert(points[i]);
skPoints[i + 1] = this.Convert(points[i + 1]);
}
break;
}
this.SkCanvas.DrawPoints(SKPointMode.Lines, skPoints, paint);
}
///
public void DrawPolygon(
IList points,
OxyColor fill,
OxyColor stroke,
double thickness,
EdgeRenderingMode edgeRenderingMode,
double[] dashArray = null,
LineJoin lineJoin = LineJoin.Miter)
{
if (!fill.IsVisible() && !(stroke.IsVisible() || thickness <= 0) || points.Count < 2)
{
return;
}
var path = this.GetPath();
var actualPoints = this.GetActualPoints(points, thickness, edgeRenderingMode);
AddPoints(actualPoints, path);
path.Close();
if (fill.IsVisible())
{
var paint = this.GetFillPaint(fill, edgeRenderingMode);
this.SkCanvas.DrawPath(path, paint);
}
if (stroke.IsVisible() && thickness > 0)
{
var paint = this.GetLinePaint(stroke, thickness, edgeRenderingMode, dashArray, lineJoin);
this.SkCanvas.DrawPath(path, paint);
}
}
///
public void DrawPolygons(
IList> polygons,
OxyColor fill,
OxyColor stroke,
double thickness,
EdgeRenderingMode edgeRenderingMode,
double[] dashArray = null,
LineJoin lineJoin = LineJoin.Miter)
{
if (!fill.IsVisible() && !(stroke.IsVisible() || thickness <= 0) || polygons.Count == 0)
{
return;
}
var path = this.GetPath();
foreach (var polygon in polygons)
{
if (polygon.Count < 2)
{
continue;
}
var actualPoints = this.GetActualPoints(polygon, thickness, edgeRenderingMode);
AddPoints(actualPoints, path);
path.Close();
}
if (fill.IsVisible())
{
var paint = this.GetFillPaint(fill, edgeRenderingMode);
this.SkCanvas.DrawPath(path, paint);
}
if (stroke.IsVisible() && thickness > 0)
{
var paint = this.GetLinePaint(stroke, thickness, edgeRenderingMode, dashArray, lineJoin);
this.SkCanvas.DrawPath(path, paint);
}
}
///
public void DrawRectangle(OxyRect rectangle, OxyColor fill, OxyColor stroke, double thickness, EdgeRenderingMode edgeRenderingMode)
{
if (!fill.IsVisible() && !(stroke.IsVisible() || thickness <= 0))
{
return;
}
var actualRectangle = this.GetActualRect(rectangle, thickness, edgeRenderingMode);
if (fill.IsVisible())
{
var paint = this.GetFillPaint(fill, edgeRenderingMode);
this.SkCanvas.DrawRect(actualRectangle, paint);
}
if (stroke.IsVisible() && thickness > 0)
{
var paint = this.GetStrokePaint(stroke, thickness, edgeRenderingMode);
this.SkCanvas.DrawRect(actualRectangle, paint);
}
}
///
public void DrawRectangles(IList rectangles, OxyColor fill, OxyColor stroke, double thickness, EdgeRenderingMode edgeRenderingMode)
{
if (!fill.IsVisible() && !(stroke.IsVisible() || thickness <= 0) || rectangles.Count == 0)
{
return;
}
var path = this.GetPath();
foreach (var rectangle in this.GetActualRects(rectangles, thickness, edgeRenderingMode))
{
path.AddRect(rectangle);
}
if (fill.IsVisible())
{
var paint = this.GetFillPaint(fill, edgeRenderingMode);
this.SkCanvas.DrawPath(path, paint);
}
if (stroke.IsVisible() && thickness > 0)
{
var paint = this.GetStrokePaint(stroke, thickness, edgeRenderingMode);
this.SkCanvas.DrawPath(path, paint);
}
}
///
public void DrawText(
ScreenPoint p,
string text,
OxyColor fill,
string fontFamily = null,
double fontSize = 10,
double fontWeight = 400,
double rotation = 0,
HorizontalAlignment horizontalAlignment = HorizontalAlignment.Left,
VerticalAlignment verticalAlignment = VerticalAlignment.Top,
OxySize? maxSize = null)
{
if (text == null || !fill.IsVisible())
{
return;
}
var paint = this.GetTextPaint(fontFamily, fontSize, fontWeight, out var shaper);
paint.Color = fill.ToSKColor();
var x = this.Convert(p.X);
var y = this.Convert(p.Y);
var lines = StringHelper.SplitLines(text);
var lineHeight = paint.GetFontMetrics(out var metrics);
var deltaY = verticalAlignment switch
{
VerticalAlignment.Top => -metrics.Ascent,
VerticalAlignment.Middle => -(metrics.Ascent + metrics.Descent + lineHeight * (lines.Length - 1)) / 2,
VerticalAlignment.Bottom => -metrics.Descent - lineHeight * (lines.Length - 1),
_ => throw new ArgumentOutOfRangeException(nameof(verticalAlignment))
};
using var _ = new SKAutoCanvasRestore(this.SkCanvas);
this.SkCanvas.Translate(x, y);
this.SkCanvas.RotateDegrees((float)rotation);
foreach (var line in lines)
{
if (this.UseTextShaping)
{
var width = this.MeasureText(line, shaper, paint);
var deltaX = horizontalAlignment switch
{
HorizontalAlignment.Left => 0,
HorizontalAlignment.Center => -width / 2,
HorizontalAlignment.Right => -width,
_ => throw new ArgumentOutOfRangeException(nameof(horizontalAlignment))
};
this.paint.TextAlign = SKTextAlign.Left;
this.SkCanvas.DrawShapedText(shaper, line, deltaX, deltaY, paint);
}
else
{
paint.TextAlign = horizontalAlignment switch
{
HorizontalAlignment.Left => SKTextAlign.Left,
HorizontalAlignment.Center => SKTextAlign.Center,
HorizontalAlignment.Right => SKTextAlign.Right,
_ => throw new ArgumentOutOfRangeException(nameof(horizontalAlignment))
};
this.SkCanvas.DrawText(line, 0, deltaY, paint);
}
deltaY += lineHeight;
}
}
///
public OxySize MeasureText(string text, string fontFamily = null, double fontSize = 10, double fontWeight = 500)
{
if (text == null)
{
return new OxySize(0, 0);
}
var lines = StringHelper.SplitLines(text);
var paint = this.GetTextPaint(fontFamily, fontSize, fontWeight, out var shaper);
var height = paint.GetFontMetrics(out _) * lines.Length;
var width = lines.Max(line => this.MeasureText(line, shaper, paint));
return new OxySize(this.ConvertBack(width), this.ConvertBack(height));
}
///
public void PopClip()
{
if (this.SkCanvas.SaveCount == 1)
{
throw new InvalidOperationException("Unbalanced call to PopClip.");
}
this.SkCanvas.Restore();
}
///
public void PushClip(OxyRect clippingRectangle)
{
this.SkCanvas.Save();
this.SkCanvas.ClipRect(this.Convert(clippingRectangle));
}
///
public void SetToolTip(string text)
{
}
///
/// Disposes managed resources.
///
/// A value indicating whether this method is called from the Dispose method.
protected virtual void Dispose(bool disposing)
{
if (!disposing)
{
return;
}
this.paint?.Dispose();
this.paint = null;
this.path?.Dispose();
this.path = null;
foreach (var typeface in this.typefaceCache.Values)
{
typeface.Dispose();
}
this.typefaceCache.Clear();
foreach (var shaper in this.shaperCache.Values)
{
shaper.Dispose();
}
this.shaperCache.Clear();
}
///
/// Adds the s to the as a series of connected lines.
///
/// The points.
/// The path.
private static void AddPoints(IEnumerable points, SKPath path)
{
using var e = points.GetEnumerator();
if (!e.MoveNext())
{
return;
}
path.MoveTo(e.Current);
while (e.MoveNext())
{
path.LineTo(e.Current);
}
}
///
/// Gets the pixel offset that a line with the specified thickness should snap to.
///
///
/// This takes into account that lines with even stroke thickness should be snapped to the border between two pixels while lines with odd stroke thickness should be snapped to the middle of a pixel.
///
/// The line thickness.
/// The snap offset.
private static float GetSnapOffset(float thickness)
{
var mod = thickness % 2;
var isOdd = mod >= 0.5 && mod < 1.5;
return isOdd ? 0.5f : 0;
}
///
/// Snaps a value to a pixel with the specified offset.
///
/// The value.
/// The offset.
/// The snapped value.
private static float Snap(float value, float offset)
{
return (float)Math.Round(value + offset, MidpointRounding.AwayFromZero) - offset;
}
///
/// Converts a to a , taking into account DPI scaling.
///
/// The rectangle.
/// The converted rectangle.
private SKRect Convert(OxyRect rect)
{
var left = this.Convert(rect.Left);
var right = this.Convert(rect.Right);
var top = this.Convert(rect.Top);
var bottom = this.Convert(rect.Bottom);
return new SKRect(left, top, right, bottom);
}
///
/// Converts a to a , taking into account DPI scaling.
///
/// The value.
/// The converted value.
private float Convert(double value)
{
return (float)value * this.DpiScale;
}
///
/// Converts to a , taking into account DPI scaling.
///
/// The point.
/// The converted point.
private SKPoint Convert(ScreenPoint point)
{
return new SKPoint(this.Convert(point.X), this.Convert(point.Y));
}
///
/// Converts a to a , applying reversed DPI scaling.
///
/// The value.
/// The converted value.
private double ConvertBack(float value)
{
return value / this.DpiScale;
}
///
/// Converts dash array to a array, taking into account DPI scaling.
///
/// The array of values.
/// The stroke thickness.
/// The array of converted values.
private float[] ConvertDashArray(double[] values, float strokeThickness)
{
var ret = new float[values.Length];
for (var i = 0; i < values.Length; i++)
{
ret[i] = this.Convert(values[i]) * strokeThickness;
}
return ret;
}
///
/// Converts a to a , taking into account DPI scaling and snapping the corners to pixels.
///
/// The rectangle.
/// The snapping offset.
/// The converted rectangle.
private SKRect ConvertSnap(OxyRect rect, float snapOffset)
{
var left = this.ConvertSnap(rect.Left, snapOffset);
var right = this.ConvertSnap(rect.Right, snapOffset);
var top = this.ConvertSnap(rect.Top, snapOffset);
var bottom = this.ConvertSnap(rect.Bottom, snapOffset);
return new SKRect(left, top, right, bottom);
}
///
/// Converts a to a , taking into account DPI scaling and snapping the value to a pixel.
///
/// The value.
/// The snapping offset.
/// The converted value.
private float ConvertSnap(double value, float snapOffset)
{
return Snap(this.Convert(value), snapOffset);
}
///
/// Converts to a , taking into account DPI scaling and snapping the point to a pixel.
///
/// The point.
/// The snapping offset.
/// The converted point.
private SKPoint ConvertSnap(ScreenPoint point, float snapOffset)
{
return new SKPoint(this.ConvertSnap(point.X, snapOffset), this.ConvertSnap(point.Y, snapOffset));
}
///
/// Gets the s that should actually be rendered from the list of s, taking into account DPI scaling and snapping if necessary.
///
/// The points.
/// The stroke thickness.
/// The edge rendering mode.
/// The actual points.
private IEnumerable GetActualPoints(IList screenPoints, double strokeThickness, EdgeRenderingMode edgeRenderingMode)
{
switch (edgeRenderingMode)
{
case EdgeRenderingMode.Automatic when this.RendersToPixels && RenderContextBase.IsStraightLine(screenPoints):
case EdgeRenderingMode.Adaptive when this.RendersToPixels && RenderContextBase.IsStraightLine(screenPoints):
case EdgeRenderingMode.PreferSharpness when this.RendersToPixels:
var snapOffset = this.GetSnapOffset(strokeThickness, edgeRenderingMode);
return screenPoints.Select(p => this.ConvertSnap(p, snapOffset));
default:
return screenPoints.Select(this.Convert);
}
}
///
/// Gets the that should actually be rendered from the , taking into account DPI scaling and snapping if necessary.
///
/// The rectangle.
/// The stroke thickness.
/// The edge rendering mode.
/// The actual rectangle.
private SKRect GetActualRect(OxyRect rect, double strokeThickness, EdgeRenderingMode edgeRenderingMode)
{
switch (edgeRenderingMode)
{
case EdgeRenderingMode.Adaptive when this.RendersToPixels:
case EdgeRenderingMode.Automatic when this.RendersToPixels:
case EdgeRenderingMode.PreferSharpness when this.RendersToPixels:
var actualThickness = this.GetActualThickness(strokeThickness, edgeRenderingMode);
var snapOffset = GetSnapOffset(actualThickness);
return this.ConvertSnap(rect, snapOffset);
default:
return this.Convert(rect);
}
}
///
/// Gets the s that should actually be rendered from the list of s, taking into account DPI scaling and snapping if necessary.
///
/// The rectangles.
/// The stroke thickness.
/// The edge rendering mode.
/// The actual rectangles.
private IEnumerable GetActualRects(IEnumerable rects, double strokeThickness, EdgeRenderingMode edgeRenderingMode)
{
switch (edgeRenderingMode)
{
case EdgeRenderingMode.Adaptive when this.RendersToPixels:
case EdgeRenderingMode.Automatic when this.RendersToPixels:
case EdgeRenderingMode.PreferSharpness when this.RendersToPixels:
var actualThickness = this.GetActualThickness(strokeThickness, edgeRenderingMode);
var snapOffset = GetSnapOffset(actualThickness);
return rects.Select(rect => this.ConvertSnap(rect, snapOffset));
default:
return rects.Select(this.Convert);
}
}
///
/// Gets the stroke thickness that should actually be used for rendering, taking into account DPI scaling and snapping if necessary.
///
/// The stroke thickness.
/// The edge rendering mode.
/// The actual stroke thickness.
private float GetActualThickness(double strokeThickness, EdgeRenderingMode edgeRenderingMode)
{
var scaledThickness = this.Convert(strokeThickness);
if (edgeRenderingMode == EdgeRenderingMode.PreferSharpness && this.RendersToPixels)
{
scaledThickness = Snap(scaledThickness, 0);
}
return scaledThickness;
}
///
/// Gets a containing information needed to render the fill of a shape.
///
///
/// This modifies and returns the local instance.
///
/// The fill color.
/// The edge rendering mode.
/// The paint.
private SKPaint GetFillPaint(OxyColor fillColor, EdgeRenderingMode edgeRenderingMode)
{
this.paint.Color = fillColor.ToSKColor();
this.paint.Style = SKPaintStyle.Fill;
this.paint.IsAntialias = this.ShouldUseAntiAliasing(edgeRenderingMode);
this.paint.PathEffect = null;
return this.paint;
}
///
/// Gets a containing information needed to render an image.
///
///
/// This modifies and returns the local instance.
///
/// The opacity.
/// A value indicating whether interpolation should be used.
/// The paint.
private SKPaint GetImagePaint(double opacity, bool interpolate)
{
this.paint.Color = new SKColor(0, 0, 0, (byte)(255 * opacity));
this.paint.FilterQuality = interpolate ? SKFilterQuality.High : SKFilterQuality.None;
this.paint.IsAntialias = true;
return this.paint;
}
///
/// Gets a containing information needed to render a line.
///
///
/// This modifies and returns the local instance.
///
/// The stroke color.
/// The stroke thickness.
/// The edge rendering mode.
/// The dash array.
/// The line join.
/// The paint.
private SKPaint GetLinePaint(OxyColor strokeColor, double strokeThickness, EdgeRenderingMode edgeRenderingMode, double[] dashArray, LineJoin lineJoin)
{
var paint = this.GetStrokePaint(strokeColor, strokeThickness, edgeRenderingMode);
if (dashArray != null)
{
var actualDashArray = this.ConvertDashArray(dashArray, paint.StrokeWidth);
paint.PathEffect = SKPathEffect.CreateDash(actualDashArray, 0);
}
paint.StrokeJoin = lineJoin switch
{
LineJoin.Miter => SKStrokeJoin.Miter,
LineJoin.Round => SKStrokeJoin.Round,
LineJoin.Bevel => SKStrokeJoin.Bevel,
_ => throw new ArgumentOutOfRangeException(nameof(lineJoin))
};
return paint;
}
///
/// Gets an empty .
///
///
/// This clears and returns the local instance.
///
/// The path.
private SKPath GetPath()
{
this.path.Reset();
return this.path;
}
///
/// Gets the snapping offset for the specified stroke thickness.
///
///
/// This takes into account that lines with even stroke thickness should be snapped to the border between two pixels while lines with odd stroke thickness should be snapped to the middle of a pixel.
///
/// The stroke thickness.
/// The edge rendering mode.
/// The snap offset.
private float GetSnapOffset(double thickness, EdgeRenderingMode edgeRenderingMode)
{
var actualThickness = this.GetActualThickness(thickness, edgeRenderingMode);
return GetSnapOffset(actualThickness);
}
///
/// Gets a containing information needed to render a stroke.
///
///
/// This modifies and returns the local instance.
///
/// The stroke color.
/// The stroke thickness.
/// The edge rendering mode.
/// The paint.
private SKPaint GetStrokePaint(OxyColor strokeColor, double strokeThickness, EdgeRenderingMode edgeRenderingMode)
{
this.paint.Color = strokeColor.ToSKColor();
this.paint.Style = SKPaintStyle.Stroke;
this.paint.IsAntialias = this.ShouldUseAntiAliasing(edgeRenderingMode);
this.paint.StrokeWidth = this.GetActualThickness(strokeThickness, edgeRenderingMode);
this.paint.PathEffect = null;
this.paint.StrokeJoin = SKStrokeJoin.Miter;
this.paint.StrokeMiter = this.MiterLimit;
return this.paint;
}
///
/// Gets a containing information needed to render text.
///
///
/// This modifies and returns the local instance.
///
/// The font family.
/// The font size.
/// The font weight.
/// The font shaper.
/// The paint.
private SKPaint GetTextPaint(string fontFamily, double fontSize, double fontWeight, out SKShaper shaper)
{
var fontDescriptor = new FontDescriptor(fontFamily, fontWeight);
if (!this.typefaceCache.TryGetValue(fontDescriptor, out var typeface))
{
typeface = SKTypeface.FromFamilyName(fontFamily, new SKFontStyle((int)fontWeight, (int)SKFontStyleWidth.Normal, SKFontStyleSlant.Upright));
#if NETSTANDARD2_0_OR_GREATER
if (typeface.FamilyName != fontFamily) // requested font not found or is WASM
{
try
{
var assembly = Assembly.GetEntryAssembly(); // the executing program (the GUI Project (WPF, WASM, ...))
var weight = (_fontWeights.ContainsKey((int)fontWeight) ? _fontWeights[(int)fontWeight] : "Regular");
var filename = $"{fontFamily}-{weight}.ttf".ToLower();
Debug.WriteLine($"Load Font {filename}");
var matches = assembly!.GetManifestResourceNames().Where(item => item.ToLower().EndsWith(filename));
if (!matches.Any()) matches = assembly!.GetManifestResourceNames().Where(item => item.ToLower().EndsWith(fontFamily + ".ttf"));
foreach (var item in matches)
{
var s = assembly.GetManifestResourceStream(item);
typeface = SKTypeface.FromStream(s);
}
}
catch
{
Debug.WriteLine($"Requested Font {fontFamily} could not be found, falling back to {typeface.FamilyName}");
}
}
#endif
this.typefaceCache.Add(fontDescriptor, typeface);
}
if (this.UseTextShaping)
{
if (!this.shaperCache.TryGetValue(fontDescriptor, out shaper))
{
shaper = new SKShaper(typeface);
this.shaperCache.Add(fontDescriptor, shaper);
}
}
else
{
shaper = null;
}
this.paint.Typeface = typeface;
this.paint.TextSize = this.Convert(fontSize);
this.paint.IsAntialias = true;
this.paint.Style = SKPaintStyle.Fill;
this.paint.HintingLevel = this.RendersToScreen ? SKPaintHinting.Full : SKPaintHinting.NoHinting;
this.paint.SubpixelText = this.RendersToScreen;
return this.paint;
}
///
/// Measures text using the specified and .
///
/// The text to measure.
/// The text shaper.
/// The paint.
/// The width of the text when rendered using the specified shaper and paint.
private float MeasureText(string text, SKShaper shaper, SKPaint paint)
{
if (!this.UseTextShaping)
{
return paint.MeasureText(text);
}
// we have to get a bit creative here as SKShaper does not offer a direct overload for this.
// see also https://github.com/mono/SkiaSharp/blob/master/source/SkiaSharp.HarfBuzz/SkiaSharp.HarfBuzz.Shared/SKShaper.cs
using var buffer = new HarfBuzzSharp.Buffer();
switch (paint.TextEncoding)
{
case SKTextEncoding.Utf8:
buffer.AddUtf8(text);
break;
case SKTextEncoding.Utf16:
buffer.AddUtf16(text);
break;
case SKTextEncoding.Utf32:
buffer.AddUtf32(text);
break;
default:
throw new NotSupportedException("TextEncoding is not supported.");
}
buffer.GuessSegmentProperties();
shaper.Shape(buffer, paint);
return buffer.GlyphPositions.Sum(gp => gp.XAdvance) * paint.TextSize / 512;
}
///
/// Gets a value indicating whether anti-aliasing should be used taking in account the specified edge rendering mode.
///
/// The edge rendering mode.
/// true if anti-aliasing should be used; false otherwise.
private bool ShouldUseAntiAliasing(EdgeRenderingMode edgeRenderingMode)
{
return edgeRenderingMode != EdgeRenderingMode.PreferSpeed;
}
///
/// Represents a font description.
///
private struct FontDescriptor
{
///
/// Initializes a new instance of the struct.
///
/// The font family.
/// The font weight.
public FontDescriptor(string fontFamily, double fontWeight)
{
this.FontFamily = fontFamily;
this.FontWeight = fontWeight;
}
///
/// The font family.
///
public string FontFamily { get; }
///
/// The font weight.
///
public double FontWeight { get; }
///
public override bool Equals(object obj)
{
return obj is FontDescriptor other && this.FontFamily == other.FontFamily && this.FontWeight == other.FontWeight;
}
///
public override int GetHashCode()
{
var hashCode = -1030903623;
hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(this.FontFamily);
hashCode = hashCode * -1521134295 + this.FontWeight.GetHashCode();
return hashCode;
}
}
}
}