Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 62 additions & 35 deletions src/Svg.Model/Drawables/Elements/TextDrawable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,66 @@ internal void EndDraw(SKCanvas skCanvas, DrawAttributes ignoreAttributes, MaskDr
skCanvas.Restore();
}

private float DrawTextRuns(
SvgTextBase svgTextBase,
string text,
float anchorX,
float anchorY,
SKRect skBounds,
SKPaint skPaint,
SKCanvas skCanvas)
{
PaintingService.SetPaintText(svgTextBase, skBounds, skPaint);

var textAlign = skPaint.TextAlign;
var typefaceSpans = AssetLoader.FindTypefaces(text, skPaint);
if (typefaceSpans.Count == 0)
{
return 0f;
}

var totalAdvance = 0f;
foreach (var span in typefaceSpans)
{
totalAdvance += span.Advance;
}

var currentX = anchorX;
if (textAlign == SKTextAlign.Center)
{
currentX -= totalAdvance * 0.5f;
}
else if (textAlign == SKTextAlign.Right)
{
currentX -= totalAdvance;
}

skPaint.TextAlign = SKTextAlign.Left;

foreach (var typefaceSpan in typefaceSpans)
{
skPaint.Typeface = typefaceSpan.Typeface;
#if USE_TEXT_SHAPER
if (skPaint.Typeface is { } typeface)
{
using var skShaper = new SKShaper(typeface);
skCanvas.DrawShapedText(skShaper, typefaceSpan.Text, currentX, anchorY, skPaint);
}
else
{
skCanvas.DrawText(typefaceSpan.Text, currentX, anchorY, skPaint);
}
#else
skPaint.TextAlign = SKTextAlign.Left;
skCanvas.DrawText(typefaceSpan.Text, currentX, anchorY, skPaint);
#endif
currentX += typefaceSpan.Advance;
skPaint = skPaint.Clone(); // Don't modify stored skPaint objects
}

return totalAdvance;
}

internal void DrawTextString(SvgTextBase svgTextBase, string text, ref float x, ref float y, SKRect skViewport, DrawAttributes ignoreAttributes, SKCanvas skCanvas, DrawableBase? until)
{
// Use element geometry for bounds so that paints relying on
Expand All @@ -311,26 +371,9 @@ internal void DrawTextString(SvgTextBase svgTextBase, string text, ref float x,
if (PaintingService.IsValidFill(svgTextBase))
{
var skPaint = PaintingService.GetFillPaint(svgTextBase, skBounds, AssetLoader, References, ignoreAttributes);

if (skPaint is { })
{
PaintingService.SetPaintText(svgTextBase, skBounds, skPaint);

foreach (var typefaceSpan in AssetLoader.FindTypefaces(text, skPaint))
{
skPaint.Typeface = typefaceSpan.Typeface;
#if USE_TEXT_SHAPER
if (skPaint.Typeface is { } typeface)
{
using var skShaper = new SKShaper(skPaint.Typeface);
skCanvas.DrawShapedText(skShaper, typefaceSpan.text, x + fillAdvance, y, skPaint);
}
#else
skCanvas.DrawText(typefaceSpan.Text, x + fillAdvance, y, skPaint);
#endif
skPaint = skPaint.Clone(); // Don't modify stored skPaint objects
fillAdvance += typefaceSpan.Advance;
}
fillAdvance = DrawTextRuns(svgTextBase, text, x, y, skBounds, skPaint, skCanvas);
}
}

Expand All @@ -341,23 +384,7 @@ internal void DrawTextString(SvgTextBase svgTextBase, string text, ref float x,
var skPaint = PaintingService.GetStrokePaint(svgTextBase, skBounds, AssetLoader, References, ignoreAttributes);
if (skPaint is { })
{
PaintingService.SetPaintText(svgTextBase, skBounds, skPaint);

foreach (var typefaceSpan in AssetLoader.FindTypefaces(text, skPaint))
{
skPaint.Typeface = typefaceSpan.Typeface;
#if USE_TEXT_SHAPER
if (skPaint.Typeface is { } typeface)
{
using var skShaper = new SKShaper(skPaint.Typeface);
skCanvas.DrawShapedText(skShaper, typefaceSpan.text, x + strokeAdvance, y, skPaint);
}
#else
skCanvas.DrawText(typefaceSpan.Text, x + strokeAdvance, y, skPaint);
#endif
skPaint = skPaint.Clone(); // Don't modify stored skPaint objects
strokeAdvance += typefaceSpan.Advance;
}
strokeAdvance = DrawTextRuns(svgTextBase, text, x, y, skBounds, skPaint, skCanvas);
}
}

Expand Down
137 changes: 126 additions & 11 deletions src/Svg.Skia/SkiaModel.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,24 @@
// Copyright (c) Wiesław Šoltés. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for details.
using System;
using System.Collections.Generic;
using ShimSkiaSharp;

namespace Svg.Skia;

public class SkiaModel
{
private static readonly char[] s_fontFamilyTrimChars = { '\'', '"' };

private static readonly Dictionary<string, string[]> s_genericFontFamilyMap = new(StringComparer.OrdinalIgnoreCase)
{
["sans-serif"] = new[] { "sans-serif", "Helvetica Neue", "Helvetica", "Arial", "Roboto", "Segoe UI", "DejaVu Sans" },
["serif"] = new[] { "serif", "Times New Roman", "Times", "Georgia", "Droid Serif", "DejaVu Serif" },
["monospace"] = new[] { "monospace", "Courier New", "Courier", "Menlo", "Consolas", "Roboto Mono", "DejaVu Sans Mono" },
["cursive"] = new[] { "cursive", "Snell Roundhand", "Comic Sans MS", "Apple Chancery" },
["fantasy"] = new[] { "fantasy", "Impact", "Papyrus" }
};

public SKSvgSettings Settings { get; }

public SkiaModel(SKSvgSettings settings)
Expand Down Expand Up @@ -178,29 +190,114 @@ public SkiaSharp.SKFontStyleSlant ToSKFontStyleSlant(SKFontStyleSlant fontStyleS
};
}

private IEnumerable<string> EnumerateFontFamilyCandidates(string? fontFamily)
{
var yielded = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

if (!string.IsNullOrWhiteSpace(fontFamily))
{
foreach (var rawFamily in fontFamily.Split(','))
{
var candidate = rawFamily.Trim();
if (candidate.Length == 0)
{
continue;
}

candidate = candidate.Trim(s_fontFamilyTrimChars);
if (candidate.Length == 0 || !yielded.Add(candidate))
{
continue;
}

yield return candidate;

if (s_genericFontFamilyMap.TryGetValue(candidate, out var mappedFamilies))
{
foreach (var mapped in mappedFamilies)
{
if (yielded.Add(mapped))
{
yield return mapped;
}
}
}
}
}

if (yielded.Count == 0 && s_genericFontFamilyMap.TryGetValue("sans-serif", out var fallbackFamilies))
{
foreach (var mapped in fallbackFamilies)
{
if (yielded.Add(mapped))
{
yield return mapped;
}
}
}
}

private SkiaSharp.SKTypeface? ResolveTypeface(string candidate, SkiaSharp.SKFontStyle style)
{
if (string.IsNullOrEmpty(candidate))
{
return null;
}

var fontManager = SkiaSharp.SKFontManager.Default;
var matched = fontManager.MatchFamily(candidate, style);
if (matched is { })
{
return matched;
}

return SkiaSharp.SKTypeface.FromFamilyName(candidate, style.Weight, style.Width, style.Slant);
}

public SkiaSharp.SKTypeface? ToSKTypeface(SKTypeface? typeface)
{
if (typeface is null || typeface.FamilyName is null)
return SkiaSharp.SKTypeface.Default;
var fontFamily = typeface?.FamilyName;
var fontWeight = ToSKFontStyleWeight(typeface?.FontWeight ?? SKFontStyleWeight.Normal);
var fontWidth = ToSKFontStyleWidth(typeface?.FontWidth ?? SKFontStyleWidth.Normal);
var fontStyle = ToSKFontStyleSlant(typeface?.FontSlant ?? SKFontStyleSlant.Upright);
var style = new SkiaSharp.SKFontStyle(fontWeight, fontWidth, fontStyle);

foreach (var candidate in EnumerateFontFamilyCandidates(fontFamily))
{
if (Settings.TypefaceProviders is { } && Settings.TypefaceProviders.Count > 0)
{
foreach (var typefaceProvider in Settings.TypefaceProviders)
{
var providerTypeface = typefaceProvider.FromFamilyName(candidate, fontWeight, fontWidth, fontStyle);
if (providerTypeface is { })
{
return providerTypeface;
}
}
}

var fontFamily = typeface.FamilyName;
var fontWeight = ToSKFontStyleWeight(typeface.FontWeight);
var fontWidth = ToSKFontStyleWidth(typeface.FontWidth);
var fontStyle = ToSKFontStyleSlant(typeface.FontSlant);
var resolved = ResolveTypeface(candidate, style);
if (resolved is { })
{
return resolved;
}
}

if (Settings.TypefaceProviders is { } && Settings.TypefaceProviders.Count > 0)
{
foreach (var typefaceProvider in Settings.TypefaceProviders)
{
var skTypeface = typefaceProvider.FromFamilyName(fontFamily, fontWeight, fontWidth, fontStyle);
if (skTypeface is { })
var providerTypeface = typefaceProvider.FromFamilyName(SkiaSharp.SKTypeface.Default.FamilyName, fontWeight, fontWidth, fontStyle);
if (providerTypeface is { })
{
return skTypeface;
return providerTypeface;
}
}
}

return SkiaSharp.SKTypeface.FromFamilyName(fontFamily, fontWeight, fontWidth, fontStyle);
var defaultTypeface = SkiaSharp.SKTypeface.FromFamilyName(null, fontWeight, fontWidth, fontStyle);

return defaultTypeface ?? SkiaSharp.SKTypeface.Default;
}

public SkiaSharp.SKColor ToSKColor(SKColor color)
Expand Down Expand Up @@ -950,7 +1047,7 @@ public SkiaSharp.SKFilterQuality ToSKFilterQuality(SKFilterQuality filterQuality
var blendMode = ToSKBlendMode(paint.BlendMode);
var filterQuality = ToSKFilterQuality(paint.FilterQuality);

return new SkiaSharp.SKPaint
var skPaint = new SkiaSharp.SKPaint
{
Style = style,
IsAntialias = paint.IsAntialias,
Expand All @@ -972,6 +1069,24 @@ public SkiaSharp.SKFilterQuality ToSKFilterQuality(SKFilterQuality filterQuality
BlendMode = blendMode,
FilterQuality = filterQuality
};

ApplyTypefaceAdjustments(paint, skPaint);

return skPaint;
}

private void ApplyTypefaceAdjustments(ShimSkiaSharp.SKPaint sourcePaint, SkiaSharp.SKPaint targetPaint)
{
if (sourcePaint.Typeface is null || targetPaint.Typeface is null)
{
return;
}

var desiredWeight = (int)ToSKFontStyleWeight(sourcePaint.Typeface.FontWeight);
if (targetPaint.Typeface.FontWeight < desiredWeight)
{
targetPaint.FakeBoldText = true;
}
}

public SkiaSharp.SKClipOperation ToSKClipOperation(SKClipOperation clipOperation)
Expand Down
63 changes: 63 additions & 0 deletions tests/Svg.Skia.UnitTests/Issue405Tests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
#pragma warning disable CS0618 // Typeface and FakeBoldText are deprecated on SKPaint; shim keeps the legacy surface for compatibility

using ShimSkiaSharp;
using Svg.Skia;
using Xunit;

namespace Svg.Skia.UnitTests;

public class Issue405Tests
{
private readonly SkiaModel _model = new(new SKSvgSettings());
private readonly SkiaSvgAssetLoader _assetLoader;

public Issue405Tests()
{
_assetLoader = new SkiaSvgAssetLoader(_model);
}

[Fact]
public void SansSerifBold_ResolvesSingleTypefaceSpan()
{
var paint = new SKPaint
{
Typeface = SKTypeface.FromFamilyName(
"sans-serif",
SKFontStyleWeight.Bold,
SKFontStyleWidth.Normal,
SKFontStyleSlant.Upright)
};

var spans = _assetLoader.FindTypefaces("Bold Text 20px", paint);

Assert.Single(spans);
var span = spans[0];
Assert.NotNull(span.Typeface);
Assert.True(span.Typeface!.FontWeight >= SKFontStyleWeight.SemiBold,
"Expected resolved typeface to be semi-bold or heavier.");
}

[Fact]
public void FakeBoldMatchesDesiredWeight()
{
var paint = new SKPaint
{
Typeface = SKTypeface.FromFamilyName(
"sans-serif",
SKFontStyleWeight.ExtraBlack,
SKFontStyleWidth.Normal,
SKFontStyleSlant.Upright)
};

using var skPaint = _model.ToSKPaint(paint);
Assert.NotNull(skPaint);

var desiredWeight = (int)SkiaSharp.SKFontStyleWeight.ExtraBlack;
var actualWeight = skPaint!.Typeface?.FontWeight ?? 0;
var shouldFakeBold = actualWeight < desiredWeight;

Assert.Equal(shouldFakeBold, skPaint.FakeBoldText);
}
}

#pragma warning restore CS0618
Loading