From 7e4f2edc311803ecd704679871a266e9ed398738 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Tue, 12 May 2026 15:59:07 +0200 Subject: [PATCH 1/6] Add SkiaSharp v3 shim APIs --- src/ShimSkiaSharp/SKCanvas.cs | 27 ++- src/ShimSkiaSharp/SKFont.cs | 174 ++++++++++++++++++ src/ShimSkiaSharp/SKSamplingOptions.cs | 62 +++++++ src/ShimSkiaSharp/SKTextBlob.cs | 12 ++ .../CloneCommandTests.cs | 5 +- .../ShimSkiaSharp.UnitTests/CloneCoreTests.cs | 26 +++ .../ShimSkiaSharp.UnitTests/CloneTestData.cs | 14 +- 7 files changed, 312 insertions(+), 8 deletions(-) create mode 100644 src/ShimSkiaSharp/SKFont.cs create mode 100644 src/ShimSkiaSharp/SKSamplingOptions.cs diff --git a/src/ShimSkiaSharp/SKCanvas.cs b/src/ShimSkiaSharp/SKCanvas.cs index 7ec406ac9a..e8d4b98ccd 100644 --- a/src/ShimSkiaSharp/SKCanvas.cs +++ b/src/ShimSkiaSharp/SKCanvas.cs @@ -30,12 +30,12 @@ internal CanvasCommand DeepClone(CloneContext context) { ClipPathCanvasCommand clipPathCanvasCommand => new ClipPathCanvasCommand(clipPathCanvasCommand.ClipPath?.DeepClone(context), clipPathCanvasCommand.Operation, clipPathCanvasCommand.Antialias), ClipRectCanvasCommand clipRectCanvasCommand => new ClipRectCanvasCommand(clipRectCanvasCommand.Rect, clipRectCanvasCommand.Operation, clipRectCanvasCommand.Antialias), - DrawImageCanvasCommand drawImageCanvasCommand => new DrawImageCanvasCommand(drawImageCanvasCommand.Image?.DeepClone(context), drawImageCanvasCommand.Source, drawImageCanvasCommand.Dest, drawImageCanvasCommand.Paint?.DeepClone(context)), + DrawImageCanvasCommand drawImageCanvasCommand => new DrawImageCanvasCommand(drawImageCanvasCommand.Image?.DeepClone(context), drawImageCanvasCommand.Source, drawImageCanvasCommand.Dest, drawImageCanvasCommand.Paint?.DeepClone(context), drawImageCanvasCommand.Sampling), DrawPictureCanvasCommand drawPictureCanvasCommand => new DrawPictureCanvasCommand(drawPictureCanvasCommand.Picture?.DeepClone(context)), DrawPathCanvasCommand drawPathCanvasCommand => new DrawPathCanvasCommand(drawPathCanvasCommand.Path?.DeepClone(context), drawPathCanvasCommand.Paint?.DeepClone(context)), DrawTextBlobCanvasCommand drawTextBlobCanvasCommand => new DrawTextBlobCanvasCommand(drawTextBlobCanvasCommand.TextBlob?.DeepClone(context), drawTextBlobCanvasCommand.X, drawTextBlobCanvasCommand.Y, drawTextBlobCanvasCommand.Paint?.DeepClone(context)), - DrawTextCanvasCommand drawTextCanvasCommand => new DrawTextCanvasCommand(drawTextCanvasCommand.Text, drawTextCanvasCommand.X, drawTextCanvasCommand.Y, drawTextCanvasCommand.Paint?.DeepClone(context)), - DrawTextOnPathCanvasCommand drawTextOnPathCanvasCommand => new DrawTextOnPathCanvasCommand(drawTextOnPathCanvasCommand.Text, drawTextOnPathCanvasCommand.Path?.DeepClone(context), drawTextOnPathCanvasCommand.HOffset, drawTextOnPathCanvasCommand.VOffset, drawTextOnPathCanvasCommand.Paint?.DeepClone(context)), + DrawTextCanvasCommand drawTextCanvasCommand => new DrawTextCanvasCommand(drawTextCanvasCommand.Text, drawTextCanvasCommand.X, drawTextCanvasCommand.Y, drawTextCanvasCommand.Paint?.DeepClone(context), drawTextCanvasCommand.TextAlign, drawTextCanvasCommand.Font?.DeepClone(context)), + DrawTextOnPathCanvasCommand drawTextOnPathCanvasCommand => new DrawTextOnPathCanvasCommand(drawTextOnPathCanvasCommand.Text, drawTextOnPathCanvasCommand.Path?.DeepClone(context), drawTextOnPathCanvasCommand.HOffset, drawTextOnPathCanvasCommand.VOffset, drawTextOnPathCanvasCommand.Paint?.DeepClone(context), drawTextOnPathCanvasCommand.TextAlign, drawTextOnPathCanvasCommand.Font?.DeepClone(context)), RestoreCanvasCommand restoreCanvasCommand => new RestoreCanvasCommand(restoreCanvasCommand.Count), SaveCanvasCommand saveCanvasCommand => new SaveCanvasCommand(saveCanvasCommand.Count), SaveLayerCanvasCommand saveLayerCanvasCommand => new SaveLayerCanvasCommand(saveLayerCanvasCommand.Count, saveLayerCanvasCommand.Paint?.DeepClone(context)), @@ -65,7 +65,7 @@ public record ClipPathCanvasCommand(ClipPath? ClipPath, SKClipOperation Operatio public record ClipRectCanvasCommand(SKRect Rect, SKClipOperation Operation, bool Antialias) : CanvasCommand; -public record DrawImageCanvasCommand(SKImage? Image, SKRect Source, SKRect Dest, SKPaint? Paint = null) : CanvasCommand; +public record DrawImageCanvasCommand(SKImage? Image, SKRect Source, SKRect Dest, SKPaint? Paint = null, SKSamplingOptions? Sampling = null) : CanvasCommand; public record DrawPictureCanvasCommand(SKPicture? Picture) : CanvasCommand; @@ -73,9 +73,9 @@ public record DrawPathCanvasCommand(SKPath? Path, SKPaint? Paint) : CanvasComman public record DrawTextBlobCanvasCommand(SKTextBlob? TextBlob, float X, float Y, SKPaint? Paint) : CanvasCommand; -public record DrawTextCanvasCommand(string Text, float X, float Y, SKPaint? Paint) : CanvasCommand; +public record DrawTextCanvasCommand(string Text, float X, float Y, SKPaint? Paint, SKTextAlign? TextAlign = null, SKFont? Font = null) : CanvasCommand; -public record DrawTextOnPathCanvasCommand(string Text, SKPath? Path, float HOffset, float VOffset, SKPaint? Paint) : CanvasCommand; +public record DrawTextOnPathCanvasCommand(string Text, SKPath? Path, float HOffset, float VOffset, SKPaint? Paint, SKTextAlign? TextAlign = null, SKFont? Font = null) : CanvasCommand; public record RestoreCanvasCommand(int Count) : CanvasCommand; @@ -206,6 +206,11 @@ public void DrawImage(SKImage image, SKRect source, SKRect dest, SKPaint? paint AddCommand(new DrawImageCanvasCommand(image, source, dest, paint)); } + public void DrawImage(SKImage image, SKRect source, SKRect dest, SKSamplingOptions sampling, SKPaint? paint = null) + { + AddCommand(new DrawImageCanvasCommand(image, source, dest, paint, sampling)); + } + public void DrawPicture(SKPicture picture) { AddCommand(new DrawPictureCanvasCommand(picture)); @@ -226,11 +231,21 @@ public void DrawText(string text, float x, float y, SKPaint paint) AddCommand(new DrawTextCanvasCommand(text, x, y, paint)); } + public void DrawText(string text, float x, float y, SKTextAlign textAlign, SKFont font, SKPaint paint) + { + AddCommand(new DrawTextCanvasCommand(text, x, y, paint, textAlign, font)); + } + public void DrawTextOnPath(string text, SKPath path, float hOffset, float vOffset, SKPaint paint) { AddCommand(new DrawTextOnPathCanvasCommand(text, path, hOffset, vOffset, paint)); } + public void DrawTextOnPath(string text, SKPath path, float hOffset, float vOffset, SKTextAlign textAlign, SKFont font, SKPaint paint) + { + AddCommand(new DrawTextOnPathCanvasCommand(text, path, hOffset, vOffset, paint, textAlign, font)); + } + public void SetMatrix(SKMatrix deltaMatrix) { TotalMatrix = TotalMatrix.PreConcat(deltaMatrix); diff --git a/src/ShimSkiaSharp/SKFont.cs b/src/ShimSkiaSharp/SKFont.cs new file mode 100644 index 0000000000..9eea05994e --- /dev/null +++ b/src/ShimSkiaSharp/SKFont.cs @@ -0,0 +1,174 @@ +// 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; + +namespace ShimSkiaSharp; + +public enum SKFontEdging +{ + Alias, + Antialias, + SubpixelAntialias +} + +public sealed class SKFont : ICloneable, IDeepCloneable +{ + private const float DefaultSize = 12f; + private const float DefaultScaleX = 1f; + private const float DefaultSkewX = 0f; + + private SKTypeface? _typeface; + private float _size = DefaultSize; + private float _scaleX = DefaultScaleX; + private float _skewX = DefaultSkewX; + private bool _subpixel; + private bool _embolden; + private SKFontEdging _edging = SKFontEdging.Antialias; + private int _version; + + public SKFont() + { + } + + public SKFont(SKTypeface? typeface, float size = DefaultSize, float scaleX = DefaultScaleX, float skewX = DefaultSkewX) + { + _typeface = typeface; + _size = size; + _scaleX = scaleX; + _skewX = skewX; + } + + internal int Version => _version; + + public SKTypeface? Typeface + { + get => _typeface; + set + { + if (ReferenceEquals(_typeface, value)) + { + return; + } + + _typeface = value; + _version++; + } + } + + public float Size + { + get => _size; + set + { + if (_size.Equals(value)) + { + return; + } + + _size = value; + _version++; + } + } + + public float ScaleX + { + get => _scaleX; + set + { + if (_scaleX.Equals(value)) + { + return; + } + + _scaleX = value; + _version++; + } + } + + public float SkewX + { + get => _skewX; + set + { + if (_skewX.Equals(value)) + { + return; + } + + _skewX = value; + _version++; + } + } + + public bool Subpixel + { + get => _subpixel; + set + { + if (_subpixel == value) + { + return; + } + + _subpixel = value; + _version++; + } + } + + public bool Embolden + { + get => _embolden; + set + { + if (_embolden == value) + { + return; + } + + _embolden = value; + _version++; + } + } + + public SKFontEdging Edging + { + get => _edging; + set + { + if (_edging == value) + { + return; + } + + _edging = value; + _version++; + } + } + + public SKFont Clone() => DeepClone(new CloneContext()); + + public SKFont DeepClone() => Clone(); + + object ICloneable.Clone() => Clone(); + + internal SKFont DeepClone(CloneContext context) + { + if (context.TryGet(this, out SKFont existing)) + { + return existing; + } + + var clone = new SKFont(); + context.Add(this, clone); + + clone.Typeface = Typeface?.DeepClone(context); + clone.Size = Size; + clone.ScaleX = ScaleX; + clone.SkewX = SkewX; + clone.Subpixel = Subpixel; + clone.Embolden = Embolden; + clone.Edging = Edging; + + return clone; + } +} diff --git a/src/ShimSkiaSharp/SKSamplingOptions.cs b/src/ShimSkiaSharp/SKSamplingOptions.cs new file mode 100644 index 0000000000..eb22bad5b1 --- /dev/null +++ b/src/ShimSkiaSharp/SKSamplingOptions.cs @@ -0,0 +1,62 @@ +// Copyright (c) Wiesław Šoltés. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. + +namespace ShimSkiaSharp; + +public enum SKFilterMode +{ + Nearest, + Linear +} + +public enum SKMipmapMode +{ + None, + Nearest, + Linear +} + +public readonly struct SKCubicResampler +{ + public static readonly SKCubicResampler Mitchell = new(1f / 3f, 1f / 3f); + public static readonly SKCubicResampler CatmullRom = new(0f, 1f / 2f); + + public SKCubicResampler(float b, float c) + { + B = b; + C = c; + } + + public float B { get; } + + public float C { get; } +} + +public readonly struct SKSamplingOptions +{ + public static readonly SKSamplingOptions Default = new(); + + public SKSamplingOptions(SKFilterMode filter, SKMipmapMode mipmap = SKMipmapMode.None) + { + Filter = filter; + Mipmap = mipmap; + Cubic = default; + UseCubic = false; + } + + public SKSamplingOptions(SKCubicResampler cubic) + { + Filter = default; + Mipmap = default; + Cubic = cubic; + UseCubic = true; + } + + public SKFilterMode Filter { get; } + + public SKMipmapMode Mipmap { get; } + + public SKCubicResampler Cubic { get; } + + public bool UseCubic { get; } +} diff --git a/src/ShimSkiaSharp/SKTextBlob.cs b/src/ShimSkiaSharp/SKTextBlob.cs index 3057031176..a0bc690253 100644 --- a/src/ShimSkiaSharp/SKTextBlob.cs +++ b/src/ShimSkiaSharp/SKTextBlob.cs @@ -9,6 +9,7 @@ public sealed class SKTextBlob : ICloneable, IDeepCloneable public string? Text { get; private set; } public ushort[]? Glyphs { get; private set; } public SKPoint[]? Points { get; private set; } + public SKFont? Font { get; private set; } private SKTextBlob() { @@ -17,6 +18,16 @@ private SKTextBlob() public static SKTextBlob CreatePositioned(string? text, SKPoint[]? points) => new() { Text = text, Points = points }; + public static SKTextBlob CreatePositioned(string? text, SKFont font, SKPoint[]? points) + { + if (font is null) + { + throw new ArgumentNullException(nameof(font)); + } + + return new() { Text = text, Font = font, Points = points }; + } + public static SKTextBlob CreatePositionedGlyphs(ushort[]? glyphs, SKPoint[]? points) => new() { Glyphs = glyphs, Points = points }; @@ -39,6 +50,7 @@ internal SKTextBlob DeepClone(CloneContext context) clone.Text = Text; clone.Glyphs = CloneHelpers.CloneArray(Glyphs, context); clone.Points = CloneHelpers.CloneArray(Points, context); + clone.Font = Font?.DeepClone(context); return clone; } diff --git a/tests/ShimSkiaSharp.UnitTests/CloneCommandTests.cs b/tests/ShimSkiaSharp.UnitTests/CloneCommandTests.cs index 4df4f8fd03..7e8b5b2fa0 100644 --- a/tests/ShimSkiaSharp.UnitTests/CloneCommandTests.cs +++ b/tests/ShimSkiaSharp.UnitTests/CloneCommandTests.cs @@ -103,7 +103,8 @@ public void CanvasCommand_DeepClone_ClonesDrawImage() { var image = CloneTestData.CreateImage(); var paint = CloneTestData.CreatePaint(); - CanvasCommand command = new DrawImageCanvasCommand(image, SKRect.Create(0, 0, 10, 10), SKRect.Create(1, 1, 5, 5), paint); + var sampling = new SKSamplingOptions(SKCubicResampler.CatmullRom); + CanvasCommand command = new DrawImageCanvasCommand(image, SKRect.Create(0, 0, 10, 10), SKRect.Create(1, 1, 5, 5), paint, sampling); var clone = command.DeepClone(); var typed = Assert.IsType(clone); @@ -113,6 +114,7 @@ public void CanvasCommand_DeepClone_ClonesDrawImage() Assert.NotSame(image.Data, typed.Image!.Data); Assert.Equal(SKRect.Create(0, 0, 10, 10), typed.Source); Assert.Equal(SKRect.Create(1, 1, 5, 5), typed.Dest); + Assert.Equal(sampling, typed.Sampling); } [Fact] @@ -161,6 +163,7 @@ public void CanvasCommand_DeepClone_ClonesDrawTextBlob() Assert.NotSame(textBlob, typed.TextBlob); Assert.NotSame(paint, typed.Paint); Assert.NotSame(textBlob.Points, typed.TextBlob!.Points); + Assert.NotSame(textBlob.Font, typed.TextBlob.Font); Assert.Equal(1, typed.X); Assert.Equal(2, typed.Y); } diff --git a/tests/ShimSkiaSharp.UnitTests/CloneCoreTests.cs b/tests/ShimSkiaSharp.UnitTests/CloneCoreTests.cs index 8e945cf876..2862f62947 100644 --- a/tests/ShimSkiaSharp.UnitTests/CloneCoreTests.cs +++ b/tests/ShimSkiaSharp.UnitTests/CloneCoreTests.cs @@ -42,6 +42,15 @@ public void SKTextBlob_Clone_DeepClone_CopiesPoints() AssertTextBlobClone(textBlob, textBlob.DeepClone()); } + [Fact] + public void SKFont_Clone_DeepClone_CopiesPropertiesAndNestedObjects() + { + var font = CloneTestData.CreateFont(); + + AssertFontClone(font, font.Clone()); + AssertFontClone(font, font.DeepClone()); + } + [Fact] public void SKTypeface_Clone_DeepClone_CopiesProperties() { @@ -236,6 +245,23 @@ private static void AssertTextBlobClone(SKTextBlob original, SKTextBlob clone) Assert.Equal(original.Text, clone.Text); Assert.NotSame(original.Points, clone.Points); Assert.Equal(original.Points, clone.Points); + AssertFontClone(original.Font!, clone.Font!); + } + + private static void AssertFontClone(SKFont original, SKFont clone) + { + Assert.NotSame(original, clone); + Assert.NotSame(original.Typeface, clone.Typeface); + Assert.Equal(original.Typeface!.FamilyName, clone.Typeface!.FamilyName); + Assert.Equal(original.Typeface.FontWeight, clone.Typeface.FontWeight); + Assert.Equal(original.Typeface.FontWidth, clone.Typeface.FontWidth); + Assert.Equal(original.Typeface.FontSlant, clone.Typeface.FontSlant); + Assert.Equal(original.Size, clone.Size); + Assert.Equal(original.ScaleX, clone.ScaleX); + Assert.Equal(original.SkewX, clone.SkewX); + Assert.Equal(original.Subpixel, clone.Subpixel); + Assert.Equal(original.Embolden, clone.Embolden); + Assert.Equal(original.Edging, clone.Edging); } private static void AssertTypefaceClone(SKTypeface original, SKTypeface clone) diff --git a/tests/ShimSkiaSharp.UnitTests/CloneTestData.cs b/tests/ShimSkiaSharp.UnitTests/CloneTestData.cs index 50ff44df49..eb4107f682 100644 --- a/tests/ShimSkiaSharp.UnitTests/CloneTestData.cs +++ b/tests/ShimSkiaSharp.UnitTests/CloneTestData.cs @@ -46,8 +46,20 @@ public static SKPath CreatePath() public static SKImage CreateImage() => new SKImage { Data = new byte[] { 1, 2, 3, 4 }, Width = 10, Height = 20 }; + public static SKFont CreateFont() + => new( + SKTypeface.FromFamilyName("Font", SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Italic), + 16, + 1.25f, + 0.1f) + { + Subpixel = true, + Embolden = true, + Edging = SKFontEdging.SubpixelAntialias + }; + public static SKTextBlob CreateTextBlob() - => SKTextBlob.CreatePositioned("Text", new[] { new SKPoint(1, 2), new SKPoint(3, 4) }); + => SKTextBlob.CreatePositioned("Text", CreateFont(), new[] { new SKPoint(1, 2), new SKPoint(3, 4) }); public static ClipPath CreateClipPath() { From cdfc0edec4eb8cb65b0fb77c0cc9b366598fc0ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Tue, 12 May 2026 15:59:17 +0200 Subject: [PATCH 2/6] Generate SkiaSharp v3 drawing APIs --- .../SkiaCSharpModelExtensions.cs | 261 +++++++++++++----- .../SkiaCSharpCodeGenTests.cs | 63 +++++ 2 files changed, 253 insertions(+), 71 deletions(-) diff --git a/src/Svg.CodeGen.Skia/SkiaCSharpModelExtensions.cs b/src/Svg.CodeGen.Skia/SkiaCSharpModelExtensions.cs index fa4711ab89..82fc41103c 100644 --- a/src/Svg.CodeGen.Skia/SkiaCSharpModelExtensions.cs +++ b/src/Svg.CodeGen.Skia/SkiaCSharpModelExtensions.cs @@ -247,6 +247,20 @@ public static string ToSKTextEncoding(this SKTextEncoding textEncoding) } } + public static string ToSKFontEdging(this SKFontEdging edging) + { + switch (edging) + { + default: + case SKFontEdging.Antialias: + return "SKFontEdging.Antialias"; + case SKFontEdging.Alias: + return "SKFontEdging.Alias"; + case SKFontEdging.SubpixelAntialias: + return "SKFontEdging.SubpixelAntialias"; + } + } + public static string ToSKFontStyleWeight(this SKFontStyleWeight fontStyleWeight) { switch (fontStyleWeight) @@ -1335,22 +1349,70 @@ public static string ToSKBlendMode(this SKBlendMode blendMode) } } - public static string ToSKFilterQuality(this SKFilterQuality filterQuality) + public static string ToSKSamplingOptions(this SKFilterQuality filterQuality) { switch (filterQuality) { default: case SKFilterQuality.None: - return "SKFilterQuality.None"; + return "new SKSamplingOptions(SKFilterMode.Nearest, SKMipmapMode.None)"; case SKFilterQuality.Low: - return "SKFilterQuality.Low"; + return "new SKSamplingOptions(SKFilterMode.Linear, SKMipmapMode.None)"; case SKFilterQuality.Medium: - return "SKFilterQuality.Medium"; + return "new SKSamplingOptions(SKFilterMode.Linear, SKMipmapMode.Linear)"; case SKFilterQuality.High: - return "SKFilterQuality.High"; + return "new SKSamplingOptions(SKCubicResampler.Mitchell)"; } } + public static string ToSKFilterMode(this SKFilterMode filterMode) + { + switch (filterMode) + { + default: + case SKFilterMode.Nearest: + return "SKFilterMode.Nearest"; + case SKFilterMode.Linear: + return "SKFilterMode.Linear"; + } + } + + public static string ToSKMipmapMode(this SKMipmapMode mipmapMode) + { + switch (mipmapMode) + { + default: + case SKMipmapMode.None: + return "SKMipmapMode.None"; + case SKMipmapMode.Nearest: + return "SKMipmapMode.Nearest"; + case SKMipmapMode.Linear: + return "SKMipmapMode.Linear"; + } + } + + public static string ToSKCubicResampler(this SKCubicResampler cubic) + { + if (cubic.Equals(SKCubicResampler.Mitchell)) + { + return "SKCubicResampler.Mitchell"; + } + + if (cubic.Equals(SKCubicResampler.CatmullRom)) + { + return "SKCubicResampler.CatmullRom"; + } + + return $"new SKCubicResampler({cubic.B.ToFloatString()}, {cubic.C.ToFloatString()})"; + } + + public static string ToSKSamplingOptions(this SKSamplingOptions samplingOptions) + { + return samplingOptions.UseCubic + ? $"new SKSamplingOptions({samplingOptions.Cubic.ToSKCubicResampler()})" + : $"new SKSamplingOptions({samplingOptions.Filter.ToSKFilterMode()}, {samplingOptions.Mipmap.ToSKMipmapMode()})"; + } + public static void ToSKPaint(this SKPaint paint, SkiaCSharpCodeGenCounter counter, StringBuilder sb, string indent) { var counterPaint = counter.Paint; @@ -1364,14 +1426,8 @@ public static void ToSKPaint(this SKPaint paint, SkiaCSharpCodeGenCounter counte // StrokeCap=Butt // StrokeJoin=Miter // StrokeMiter=4 - // TextSize=12 - // TextAlign=Left - // LcdRenderText=false - // SubpixelText=false - // TextEncoding=Utf8 // Color=#ff000000 // BlendMode=SrcOver - // FilterQuality=None if (paint.Style != SKPaintStyle.Fill) { @@ -1403,42 +1459,6 @@ public static void ToSKPaint(this SKPaint paint, SkiaCSharpCodeGenCounter counte sb.AppendLine($"{indent}{counter.PaintVarName}{counterPaint}.StrokeMiter = {paint.StrokeMiter.ToFloatString()};"); } - if (paint.TextSize != 12f) - { - sb.AppendLine($"{indent}{counter.PaintVarName}{counterPaint}.TextSize = {paint.TextSize.ToFloatString()};"); - } - - if (paint.TextAlign != SKTextAlign.Left) - { - sb.AppendLine($"{indent}{counter.PaintVarName}{counterPaint}.TextAlign = {paint.TextAlign.ToSKTextAlign()};"); - } - - if (paint.Typeface is { }) - { - var counterTypeface = ++counter.Typeface; - paint.Typeface?.ToSKTypeface(counter, sb, indent); - sb.AppendLine($"{indent}if ({counter.TypefaceVarName}{counterTypeface} is null)"); - sb.AppendLine($"{indent}{{"); - sb.AppendLine($"{indent} {counter.TypefaceVarName}{counterTypeface} = SKTypeface.Default;"); - sb.AppendLine($"{indent}}}"); - sb.AppendLine($"{indent}{counter.PaintVarName}{counterPaint}.Typeface = {counter.TypefaceVarName}{counterTypeface};"); - } - - if (paint.LcdRenderText != false) - { - sb.AppendLine($"{indent}{counter.PaintVarName}{counterPaint}.LcdRenderText = {paint.LcdRenderText.ToBoolString()};"); - } - - if (paint.SubpixelText != false) - { - sb.AppendLine($"{indent}{counter.PaintVarName}{counterPaint}.SubpixelText = {paint.SubpixelText.ToBoolString()};"); - } - - if (paint.TextEncoding != SKTextEncoding.Utf8) - { - sb.AppendLine($"{indent}{counter.PaintVarName}{counterPaint}.TextEncoding = {paint.TextEncoding.ToSKTextEncoding()};"); - } - if (paint.Color is { }) { // Skip default color @@ -1480,10 +1500,68 @@ public static void ToSKPaint(this SKPaint paint, SkiaCSharpCodeGenCounter counte { sb.AppendLine($"{indent}{counter.PaintVarName}{counterPaint}.BlendMode = {paint.BlendMode.ToSKBlendMode()};"); } + } + + public static void ToSKFont(this SKPaint paint, SkiaCSharpCodeGenCounter counter, StringBuilder sb, string indent) + { + var counterFont = counter.Font; + var typefaceExpression = "SKTypeface.Default"; - if (paint.FilterQuality != SKFilterQuality.None) + if (paint.Typeface is { }) { - sb.AppendLine($"{indent}{counter.PaintVarName}{counterPaint}.FilterQuality = {paint.FilterQuality.ToSKFilterQuality()};"); + var counterTypeface = ++counter.Typeface; + paint.Typeface?.ToSKTypeface(counter, sb, indent); + sb.AppendLine($"{indent}if ({counter.TypefaceVarName}{counterTypeface} is null)"); + sb.AppendLine($"{indent}{{"); + sb.AppendLine($"{indent} {counter.TypefaceVarName}{counterTypeface} = SKTypeface.Default;"); + sb.AppendLine($"{indent}}}"); + typefaceExpression = $"{counter.TypefaceVarName}{counterTypeface}"; + } + + sb.AppendLine($"{indent}var {counter.FontVarName}{counterFont} = new SKFont({typefaceExpression}, {paint.TextSize.ToFloatString()});"); + + if (paint.LcdRenderText != false) + { + sb.AppendLine($"{indent}{counter.FontVarName}{counterFont}.Edging = SKFontEdging.SubpixelAntialias;"); + } + + if (paint.SubpixelText != false) + { + sb.AppendLine($"{indent}{counter.FontVarName}{counterFont}.Subpixel = {paint.SubpixelText.ToBoolString()};"); + } + } + + public static void ToSKFont(this SKFont font, SkiaCSharpCodeGenCounter counter, StringBuilder sb, string indent) + { + var counterFont = counter.Font; + var typefaceExpression = "SKTypeface.Default"; + + if (font.Typeface is { }) + { + var counterTypeface = ++counter.Typeface; + font.Typeface?.ToSKTypeface(counter, sb, indent); + sb.AppendLine($"{indent}if ({counter.TypefaceVarName}{counterTypeface} is null)"); + sb.AppendLine($"{indent}{{"); + sb.AppendLine($"{indent} {counter.TypefaceVarName}{counterTypeface} = SKTypeface.Default;"); + sb.AppendLine($"{indent}}}"); + typefaceExpression = $"{counter.TypefaceVarName}{counterTypeface}"; + } + + sb.AppendLine($"{indent}var {counter.FontVarName}{counterFont} = new SKFont({typefaceExpression}, {font.Size.ToFloatString()}, {font.ScaleX.ToFloatString()}, {font.SkewX.ToFloatString()});"); + + if (font.Edging != SKFontEdging.Antialias) + { + sb.AppendLine($"{indent}{counter.FontVarName}{counterFont}.Edging = {font.Edging.ToSKFontEdging()};"); + } + + if (font.Subpixel != false) + { + sb.AppendLine($"{indent}{counter.FontVarName}{counterFont}.Subpixel = {font.Subpixel.ToBoolString()};"); + } + + if (font.Embolden != false) + { + sb.AppendLine($"{indent}{counter.FontVarName}{counterFont}.Embolden = {font.Embolden.ToBoolString()};"); } } @@ -1856,9 +1934,16 @@ public static void ToSKPicture(this SKPicture? picture, SkiaCSharpCodeGenCounter drawImageCanvasCommand.Image.ToSKImage(counter, sb, indent); var source = drawImageCanvasCommand.Source.ToSKRect(); var dest = drawImageCanvasCommand.Dest.ToSKRect(); - var counterPaint = ++counter.Paint; - drawImageCanvasCommand.Paint?.ToSKPaint(counter, sb, indent); - sb.AppendLine($"{indent}{counter.CanvasVarName}{counterCanvas}.DrawImage({counter.ImageVarName}{counterImage}, {source}, {dest}, {counter.PaintVarName}{counterPaint});"); + var paintExpression = "null"; + var counterPaint = -1; + if (drawImageCanvasCommand.Paint is { }) + { + counterPaint = ++counter.Paint; + drawImageCanvasCommand.Paint.ToSKPaint(counter, sb, indent); + paintExpression = $"{counter.PaintVarName}{counterPaint}"; + } + var samplingOptions = drawImageCanvasCommand.Sampling?.ToSKSamplingOptions() ?? drawImageCanvasCommand.Paint?.FilterQuality.ToSKSamplingOptions() ?? "SKSamplingOptions.Default"; + sb.AppendLine($"{indent}{counter.CanvasVarName}{counterCanvas}.DrawImage({counter.ImageVarName}{counterImage}, {source}, {dest}, {samplingOptions}, {paintExpression});"); sb.AppendLine($"{indent}{counter.ImageVarName}{counterImage}?.Dispose();"); // NOTE: Do not dispose created SKTypeface by font manager. @@ -1871,24 +1956,27 @@ public static void ToSKPicture(this SKPicture? picture, SkiaCSharpCodeGenCounter sb.AppendLine($"{indent}}}"); } #endif - if (drawImageCanvasCommand.Paint?.Shader is { }) - { - sb.AppendLine($"{indent}{counter.PaintVarName}{counterPaint}?.Shader?.Dispose();"); - } - if (drawImageCanvasCommand.Paint?.ColorFilter is { }) - { - sb.AppendLine($"{indent}{counter.PaintVarName}{counterPaint}?.ColorFilter?.Dispose();"); - } - if (drawImageCanvasCommand.Paint?.ImageFilter is { }) - { - sb.AppendLine($"{indent}{counter.PaintVarName}{counterPaint}?.ImageFilter?.Dispose();"); - } - if (drawImageCanvasCommand.Paint?.PathEffect is { }) + if (drawImageCanvasCommand.Paint is { }) { - sb.AppendLine($"{indent}{counter.PaintVarName}{counterPaint}?.PathEffect?.Dispose();"); + if (drawImageCanvasCommand.Paint.Shader is { }) + { + sb.AppendLine($"{indent}{counter.PaintVarName}{counterPaint}?.Shader?.Dispose();"); + } + if (drawImageCanvasCommand.Paint.ColorFilter is { }) + { + sb.AppendLine($"{indent}{counter.PaintVarName}{counterPaint}?.ColorFilter?.Dispose();"); + } + if (drawImageCanvasCommand.Paint.ImageFilter is { }) + { + sb.AppendLine($"{indent}{counter.PaintVarName}{counterPaint}?.ImageFilter?.Dispose();"); + } + if (drawImageCanvasCommand.Paint.PathEffect is { }) + { + sb.AppendLine($"{indent}{counter.PaintVarName}{counterPaint}?.PathEffect?.Dispose();"); + } + + sb.AppendLine($"{indent}{counter.PaintVarName}{counterPaint}?.Dispose();"); } - - sb.AppendLine($"{indent}{counter.PaintVarName}{counterPaint}?.Dispose();"); } break; } @@ -1976,7 +2064,14 @@ public static void ToSKPicture(this SKPicture? picture, SkiaCSharpCodeGenCounter var counterPaint = ++counter.Paint; drawPositionedTextCanvasCommand.Paint.ToSKPaint(counter, sb, indent); var counterFont = ++counter.Font; - sb.AppendLine($"{indent}var {counter.FontVarName}{counterFont} = {counter.PaintVarName}{counterPaint}.ToFont();"); + if (drawPositionedTextCanvasCommand.TextBlob.Font is { } textBlobFont) + { + textBlobFont.ToSKFont(counter, sb, indent); + } + else + { + drawPositionedTextCanvasCommand.Paint.ToSKFont(counter, sb, indent); + } var counterTextBlob = ++counter.TextBlob; sb.AppendLine($"{indent}var {counter.TextBlobVarName}{counterTextBlob} = SKTextBlob.CreatePositioned(\"{text}\", {counter.FontVarName}{counterFont}, {points});"); var x = drawPositionedTextCanvasCommand.X; @@ -2011,6 +2106,8 @@ public static void ToSKPicture(this SKPicture? picture, SkiaCSharpCodeGenCounter } sb.AppendLine($"{indent}{counter.PaintVarName}{counterPaint}?.Dispose();"); + sb.AppendLine($"{indent}{counter.FontVarName}{counterFont}?.Dispose();"); + sb.AppendLine($"{indent}{counter.TextBlobVarName}{counterTextBlob}?.Dispose();"); } break; } @@ -2023,7 +2120,17 @@ public static void ToSKPicture(this SKPicture? picture, SkiaCSharpCodeGenCounter var y = drawTextCanvasCommand.Y; var counterPaint = ++counter.Paint; drawTextCanvasCommand.Paint.ToSKPaint(counter, sb, indent); - sb.AppendLine($"{indent}{counter.CanvasVarName}{counterCanvas}.DrawText(\"{text}\", {x.ToFloatString()}, {y.ToFloatString()}, {counter.PaintVarName}{counterPaint});"); + var counterFont = ++counter.Font; + if (drawTextCanvasCommand.Font is { } textFont) + { + textFont.ToSKFont(counter, sb, indent); + } + else + { + drawTextCanvasCommand.Paint.ToSKFont(counter, sb, indent); + } + var textAlign = (drawTextCanvasCommand.TextAlign ?? drawTextCanvasCommand.Paint.TextAlign).ToSKTextAlign(); + sb.AppendLine($"{indent}{counter.CanvasVarName}{counterCanvas}.DrawText(\"{text}\", {x.ToFloatString()}, {y.ToFloatString()}, {textAlign}, {counter.FontVarName}{counterFont}, {counter.PaintVarName}{counterPaint});"); // NOTE: Do not dispose created SKTypeface by font manager. #if USE_DISPOSE_TYPEFACE @@ -2053,6 +2160,7 @@ public static void ToSKPicture(this SKPicture? picture, SkiaCSharpCodeGenCounter } sb.AppendLine($"{indent}{counter.PaintVarName}{counterPaint}?.Dispose();"); + sb.AppendLine($"{indent}{counter.FontVarName}{counterFont}?.Dispose();"); } break; } @@ -2067,7 +2175,17 @@ public static void ToSKPicture(this SKPicture? picture, SkiaCSharpCodeGenCounter var vOffset = drawTextOnPathCanvasCommand.VOffset; var counterPaint = ++counter.Paint; drawTextOnPathCanvasCommand.Paint.ToSKPaint(counter, sb, indent); - sb.AppendLine($"{indent}{counter.CanvasVarName}{counterCanvas}.DrawTextOnPath(\"{text}\", {counter.PathVarName}{counterPath}, {hOffset.ToFloatString()}, {vOffset.ToFloatString()}, {counter.PaintVarName}{counterPaint});"); + var counterFont = ++counter.Font; + if (drawTextOnPathCanvasCommand.Font is { } textOnPathFont) + { + textOnPathFont.ToSKFont(counter, sb, indent); + } + else + { + drawTextOnPathCanvasCommand.Paint.ToSKFont(counter, sb, indent); + } + var textAlign = (drawTextOnPathCanvasCommand.TextAlign ?? drawTextOnPathCanvasCommand.Paint.TextAlign).ToSKTextAlign(); + sb.AppendLine($"{indent}{counter.CanvasVarName}{counterCanvas}.DrawTextOnPath(\"{text}\", {counter.PathVarName}{counterPath}, {hOffset.ToFloatString()}, {vOffset.ToFloatString()}, {textAlign}, {counter.FontVarName}{counterFont}, {counter.PaintVarName}{counterPaint});"); // NOTE: Do not dispose created SKTypeface by font manager. #if USE_DISPOSE_TYPEFACE @@ -2097,6 +2215,7 @@ public static void ToSKPicture(this SKPicture? picture, SkiaCSharpCodeGenCounter } sb.AppendLine($"{indent}{counter.PaintVarName}{counterPaint}?.Dispose();"); + sb.AppendLine($"{indent}{counter.FontVarName}{counterFont}?.Dispose();"); sb.AppendLine($"{indent}{counter.PathVarName}{counterPath}?.Dispose();"); } break; diff --git a/tests/Svg.Skia.UnitTests/SkiaCSharpCodeGenTests.cs b/tests/Svg.Skia.UnitTests/SkiaCSharpCodeGenTests.cs index f5d0490fed..359293db14 100644 --- a/tests/Svg.Skia.UnitTests/SkiaCSharpCodeGenTests.cs +++ b/tests/Svg.Skia.UnitTests/SkiaCSharpCodeGenTests.cs @@ -95,6 +95,69 @@ public void Generate_DoesNotEmitNaNGradientStopsForZeroWidthGradientBounds() Assert.DoesNotContain("NaN", code); } + [Fact] + public void Generate_UsesExplicitSamplingOptionsForDrawImage() + { + var image = new SKImage { Data = new byte[] { 1, 2, 3, 4 }, Width = 1, Height = 1 }; + var picture = new SKPicture(SKRect.Create(0f, 0f, 1f, 1f), new List + { + new DrawImageCanvasCommand( + image, + SKRect.Create(0f, 0f, 1f, 1f), + SKRect.Create(0f, 0f, 1f, 1f), + null, + new SKSamplingOptions(SKCubicResampler.CatmullRom)) + }); + + var code = SkiaCSharpCodeGen.Generate(picture, "Svg", "Generated"); + + Assert.Contains("new SKSamplingOptions(SKCubicResampler.CatmullRom)", code); + Assert.DoesNotContain("FilterQuality", code); + } + + [Fact] + public void Generate_MapsLegacyFilterQualityToSamplingOptionsForDrawImage() + { + var image = new SKImage { Data = new byte[] { 1, 2, 3, 4 }, Width = 1, Height = 1 }; + var paint = new SKPaint { FilterQuality = SKFilterQuality.Medium }; + var picture = new SKPicture(SKRect.Create(0f, 0f, 1f, 1f), new List + { + new DrawImageCanvasCommand( + image, + SKRect.Create(0f, 0f, 1f, 1f), + SKRect.Create(0f, 0f, 1f, 1f), + paint) + }); + + var code = SkiaCSharpCodeGen.Generate(picture, "Svg", "Generated"); + + Assert.Contains("new SKSamplingOptions(SKFilterMode.Linear, SKMipmapMode.Linear)", code); + Assert.DoesNotContain(".FilterQuality", code); + } + + [Fact] + public void Generate_UsesTextBlobFontForPositionedText() + { + var font = new SKFont(null, 18f) + { + Edging = SKFontEdging.Alias, + Subpixel = true + }; + var textBlob = SKTextBlob.CreatePositioned("Text", font, new[] { new SKPoint(1f, 2f) }); + var picture = new SKPicture(SKRect.Create(0f, 0f, 10f, 10f), new List + { + new DrawTextBlobCanvasCommand(textBlob, 3f, 4f, new SKPaint()) + }); + + var code = SkiaCSharpCodeGen.Generate(picture, "Svg", "Generated"); + + Assert.Contains("new SKFont(SKTypeface.Default, 18f, 1f, 0f)", code); + Assert.Contains(".Edging = SKFontEdging.Alias;", code); + Assert.Contains(".Subpixel = true;", code); + Assert.Contains("SKTextBlob.CreatePositioned(\"Text\", skFont0", code); + Assert.DoesNotContain(".ToFont()", code); + } + [Fact] public void SkiaModel_ToSKShader_CreatesGradientsWithOptionalColorPositions() { From 496678a55826358c032a7c1f31257f7f52713625 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Tue, 12 May 2026 15:59:27 +0200 Subject: [PATCH 3/6] Tighten text nullable flow --- src/Svg.Skia/SkiaModel.TextShaping.cs | 2 +- src/Svg.SourceGenerator.Skia/SkiaGeneratorSvgAssetLoader.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Svg.Skia/SkiaModel.TextShaping.cs b/src/Svg.Skia/SkiaModel.TextShaping.cs index 61eca410ce..e6148cd819 100644 --- a/src/Svg.Skia/SkiaModel.TextShaping.cs +++ b/src/Svg.Skia/SkiaModel.TextShaping.cs @@ -113,7 +113,7 @@ internal bool TryShapeGlyphRun(string? text, SKPaint paint, bool? rightToLeft, o } using var skPaint = ToSKPaint(paint); - if (skPaint is null || !TryShapeText(text, 0f, 0f, skPaint, rightToLeft, out var result)) + if (skPaint is null || !TryShapeText(text!, 0f, 0f, skPaint, rightToLeft, out var result)) { return false; } diff --git a/src/Svg.SourceGenerator.Skia/SkiaGeneratorSvgAssetLoader.cs b/src/Svg.SourceGenerator.Skia/SkiaGeneratorSvgAssetLoader.cs index e875284576..51aa63d38b 100644 --- a/src/Svg.SourceGenerator.Skia/SkiaGeneratorSvgAssetLoader.cs +++ b/src/Svg.SourceGenerator.Skia/SkiaGeneratorSvgAssetLoader.cs @@ -55,7 +55,7 @@ public float MeasureText(string? text, ShimSkiaSharp.SKPaint paint, ref ShimSkia } var size = paint.TextSize; - var width = text.Length * size * 0.6f; + var width = text!.Length * size * 0.6f; bounds = new ShimSkiaSharp.SKRect(0, -size * 0.8f, width, size * 0.2f); return width; } From 568a1081a9bbc4720aecaa0b3507ef49330aa46f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Tue, 12 May 2026 16:54:28 +0200 Subject: [PATCH 4/6] Honor retained font and sampling replay --- src/Svg.Skia/SkiaModel.Caching.cs | 52 ++--- src/Svg.Skia/SkiaModel.TextShaping.cs | 30 ++- src/Svg.Skia/SkiaModel.cs | 217 ++++++++++++++++-- .../SKSvgRebuildFromModelTests.cs | 117 ++++++++++ 4 files changed, 362 insertions(+), 54 deletions(-) diff --git a/src/Svg.Skia/SkiaModel.Caching.cs b/src/Svg.Skia/SkiaModel.Caching.cs index 99f53972ad..dd0880103e 100644 --- a/src/Svg.Skia/SkiaModel.Caching.cs +++ b/src/Svg.Skia/SkiaModel.Caching.cs @@ -108,34 +108,34 @@ public TypefaceResolution(SkiaSharp.SKTypeface typeface, bool suppressSyntheticB private readonly struct FontSignature : IEquatable { - public FontSignature(SkiaSharp.SKPaint paint) + public FontSignature(SkiaSharp.SKFont font) { - TypefaceHandle = paint.Typeface?.Handle ?? IntPtr.Zero; - TextSize = paint.TextSize; - TextScaleX = paint.TextScaleX; - TextSkewX = paint.TextSkewX; - LcdRenderText = paint.LcdRenderText; - SubpixelText = paint.SubpixelText; - FakeBoldText = paint.FakeBoldText; + TypefaceHandle = font.Typeface?.Handle ?? IntPtr.Zero; + Size = font.Size; + ScaleX = font.ScaleX; + SkewX = font.SkewX; + Edging = font.Edging; + Subpixel = font.Subpixel; + Embolden = font.Embolden; } public IntPtr TypefaceHandle { get; } - public float TextSize { get; } - public float TextScaleX { get; } - public float TextSkewX { get; } - public bool LcdRenderText { get; } - public bool SubpixelText { get; } - public bool FakeBoldText { get; } + public float Size { get; } + public float ScaleX { get; } + public float SkewX { get; } + public SkiaSharp.SKFontEdging Edging { get; } + public bool Subpixel { get; } + public bool Embolden { get; } public bool Equals(FontSignature other) { return TypefaceHandle == other.TypefaceHandle - && TextSize.Equals(other.TextSize) - && TextScaleX.Equals(other.TextScaleX) - && TextSkewX.Equals(other.TextSkewX) - && LcdRenderText == other.LcdRenderText - && SubpixelText == other.SubpixelText - && FakeBoldText == other.FakeBoldText; + && Size.Equals(other.Size) + && ScaleX.Equals(other.ScaleX) + && SkewX.Equals(other.SkewX) + && Edging == other.Edging + && Subpixel == other.Subpixel + && Embolden == other.Embolden; } public override bool Equals(object? obj) @@ -148,12 +148,12 @@ public override int GetHashCode() unchecked { var hash = TypefaceHandle.GetHashCode(); - hash = (hash * 397) ^ TextSize.GetHashCode(); - hash = (hash * 397) ^ TextScaleX.GetHashCode(); - hash = (hash * 397) ^ TextSkewX.GetHashCode(); - hash = (hash * 397) ^ LcdRenderText.GetHashCode(); - hash = (hash * 397) ^ SubpixelText.GetHashCode(); - hash = (hash * 397) ^ FakeBoldText.GetHashCode(); + hash = (hash * 397) ^ Size.GetHashCode(); + hash = (hash * 397) ^ ScaleX.GetHashCode(); + hash = (hash * 397) ^ SkewX.GetHashCode(); + hash = (hash * 397) ^ Edging.GetHashCode(); + hash = (hash * 397) ^ Subpixel.GetHashCode(); + hash = (hash * 397) ^ Embolden.GetHashCode(); return hash; } } diff --git a/src/Svg.Skia/SkiaModel.TextShaping.cs b/src/Svg.Skia/SkiaModel.TextShaping.cs index e6148cd819..09cf9fe159 100644 --- a/src/Svg.Skia/SkiaModel.TextShaping.cs +++ b/src/Svg.Skia/SkiaModel.TextShaping.cs @@ -19,20 +19,16 @@ private bool TryDrawShapedText( string text, float x, float y, + SkiaSharp.SKTextAlign textAlign, + SkiaSharp.SKFont font, SkiaSharp.SKPaint paint) { - if (!TryShapeText(text, x, y, paint, rightToLeft: null, out var result)) + if (!TryShapeText(text, x, y, font, rightToLeft: null, out var result)) { return false; } using var builder = new SkiaSharp.SKTextBlobBuilder(); - using var font = paint.ToFont(); - if (font is null) - { - return false; - } - var glyphs = new ushort[result.Codepoints.Length]; for (var i = 0; i < result.Codepoints.Length; i++) { @@ -46,7 +42,7 @@ private bool TryDrawShapedText( return false; } - var xOffset = paint.TextAlign switch + var xOffset = textAlign switch { SkiaSharp.SKTextAlign.Center => -(result.Width * 0.5f), SkiaSharp.SKTextAlign.Right => -result.Width, @@ -150,6 +146,24 @@ private bool TryShapeText( return false; } + return TryShapeText(text, x, y, font, rightToLeft, out result); + } + + private static bool TryShapeText( + string text, + float x, + float y, + SkiaSharp.SKFont font, + bool? rightToLeft, + out ShapedTextResult result) + { + if (string.IsNullOrEmpty(text) || + font.Typeface is null) + { + result = default; + return false; + } + if (!HarfBuzzTextShaper.TryCreate(font.Typeface, out var shaper)) { result = default; diff --git a/src/Svg.Skia/SkiaModel.cs b/src/Svg.Skia/SkiaModel.cs index dabfd33ac0..8c1661637a 100644 --- a/src/Svg.Skia/SkiaModel.cs +++ b/src/Svg.Skia/SkiaModel.cs @@ -153,6 +153,17 @@ public SkiaSharp.SKTextEncoding ToSKTextEncoding(SKTextEncoding textEncoding) }; } + public SkiaSharp.SKFontEdging ToSKFontEdging(SKFontEdging edging) + { + return edging switch + { + SKFontEdging.Alias => SkiaSharp.SKFontEdging.Alias, + SKFontEdging.Antialias => SkiaSharp.SKFontEdging.Antialias, + SKFontEdging.SubpixelAntialias => SkiaSharp.SKFontEdging.SubpixelAntialias, + _ => SkiaSharp.SKFontEdging.Antialias + }; + } + public SkiaSharp.SKFontStyleWeight ToSKFontStyleWeight(SKFontStyleWeight fontStyleWeight) { return fontStyleWeight switch @@ -1314,6 +1325,92 @@ public SkiaSharp.SKFilterQuality ToSKFilterQuality(SKFilterQuality filterQuality }; } + public SkiaSharp.SKFilterQuality ToSKFilterQuality(SKSamplingOptions samplingOptions) + { + if (samplingOptions.UseCubic) + { + return SkiaSharp.SKFilterQuality.High; + } + + if (samplingOptions.Filter == SKFilterMode.Nearest) + { + return SkiaSharp.SKFilterQuality.None; + } + + return samplingOptions.Mipmap == SKMipmapMode.None + ? SkiaSharp.SKFilterQuality.Low + : SkiaSharp.SKFilterQuality.Medium; + } + + private static SkiaSharp.SKPaint CreateTextRenderPaint( + SkiaSharp.SKPaint paint, + SkiaSharp.SKTextAlign textAlign, + SkiaSharp.SKFont font, + bool applyFont) + { + var textPaint = paint.Clone(); + textPaint.TextAlign = textAlign; + + if (applyFont) + { + ApplyFontToPaint(font, textPaint); + } + + return textPaint; + } + + private static void ApplyFontToPaint(SkiaSharp.SKFont font, SkiaSharp.SKPaint paint) + { + paint.Typeface = font.Typeface; + paint.TextSize = font.Size; + paint.TextScaleX = font.ScaleX; + paint.TextSkewX = font.SkewX; + paint.SubpixelText = font.Subpixel; + paint.FakeBoldText = font.Embolden; + paint.LcdRenderText = font.Edging == SkiaSharp.SKFontEdging.SubpixelAntialias; + paint.IsAntialias = font.Edging != SkiaSharp.SKFontEdging.Alias; + } + + private static void DisposeIfCloned(SkiaSharp.SKPaint paint, SkiaSharp.SKPaint sourcePaint) + { + if (!ReferenceEquals(paint, sourcePaint)) + { + paint.Dispose(); + } + } + + public SkiaSharp.SKFont ToSKFont(SKPaint paint) + { + var typefaceResolution = ResolveSKTypeface(paint.Typeface); + var skFont = new SkiaSharp.SKFont(typefaceResolution.Typeface, paint.TextSize) + { + Edging = paint.LcdRenderText ? SkiaSharp.SKFontEdging.SubpixelAntialias : SkiaSharp.SKFontEdging.Antialias, + Subpixel = paint.SubpixelText + }; + + ApplyTypefaceAdjustments(paint, skFont, typefaceResolution.SuppressSyntheticBold); + return skFont; + } + + public SkiaSharp.SKFont? ToSKFont(SKFont? font) + { + if (font is null) + { + return null; + } + + var typefaceResolution = ResolveSKTypeface(font.Typeface); + var skFont = new SkiaSharp.SKFont(typefaceResolution.Typeface, font.Size, font.ScaleX, font.SkewX) + { + Edging = ToSKFontEdging(font.Edging), + Subpixel = font.Subpixel, + Embolden = font.Embolden + }; + + ApplyTypefaceAdjustments(font, skFont, typefaceResolution.SuppressSyntheticBold); + return skFont; + } + public SkiaSharp.SKPaint? ToSKPaint(SKPaint? paint) { if (paint is null) @@ -1368,26 +1465,42 @@ public SkiaSharp.SKFilterQuality ToSKFilterQuality(SKFilterQuality filterQuality private void ApplyTypefaceAdjustments(ShimSkiaSharp.SKPaint sourcePaint, SkiaSharp.SKPaint targetPaint, bool suppressSyntheticBold) { - if (suppressSyntheticBold) + if (ShouldEmboldenTypeface(sourcePaint.Typeface, targetPaint.Typeface, suppressSyntheticBold)) { - return; + targetPaint.FakeBoldText = true; } + } - if (sourcePaint.Typeface is null || targetPaint.Typeface is null) + private void ApplyTypefaceAdjustments(ShimSkiaSharp.SKPaint sourcePaint, SkiaSharp.SKFont targetFont, bool suppressSyntheticBold) + { + if (ShouldEmboldenTypeface(sourcePaint.Typeface, targetFont.Typeface, suppressSyntheticBold)) { - return; + targetFont.Embolden = true; } + } - var desiredWeight = (int)ToSKFontStyleWeight(sourcePaint.Typeface.FontWeight); - if (targetPaint.Typeface.FontWeight < desiredWeight) + private void ApplyTypefaceAdjustments(ShimSkiaSharp.SKFont sourceFont, SkiaSharp.SKFont targetFont, bool suppressSyntheticBold) + { + if (ShouldEmboldenTypeface(sourceFont.Typeface, targetFont.Typeface, suppressSyntheticBold)) { - targetPaint.FakeBoldText = true; + targetFont.Embolden = true; + } + } + + private bool ShouldEmboldenTypeface(SKTypeface? sourceTypeface, SkiaSharp.SKTypeface? targetTypeface, bool suppressSyntheticBold) + { + if (suppressSyntheticBold || sourceTypeface is null || targetTypeface is null) + { + return false; } + + var desiredWeight = (int)ToSKFontStyleWeight(sourceTypeface.FontWeight); + return targetTypeface.FontWeight < desiredWeight; } private SkiaSharp.SKTextBlob? GetCachedPositionedTextBlob( DrawTextBlobCanvasCommand command, - SkiaSharp.SKPaint paint) + SkiaSharp.SKFont font) { var textBlob = command.TextBlob; if (textBlob?.Points is null) @@ -1395,7 +1508,7 @@ private void ApplyTypefaceAdjustments(ShimSkiaSharp.SKPaint sourcePaint, SkiaSha return null; } - var signature = new FontSignature(paint); + var signature = new FontSignature(font); lock (_positionedTextCacheLock) { PositionedTextCache? cached = null; @@ -1418,12 +1531,6 @@ private void ApplyTypefaceAdjustments(ShimSkiaSharp.SKPaint sourcePaint, SkiaSha _positionedTextCache.Remove(command); } - using var font = paint.ToFont(); - if (font is null) - { - return null; - } - var points = ToSKPoints(textBlob.Points); SkiaSharp.SKTextBlob? created; if (textBlob.Glyphs is { Length: > 0 }) @@ -1846,7 +1953,24 @@ public void Draw(CanvasCommand canvasCommand, SkiaSharp.SKCanvas skCanvas, bool var source = ToSKRect(drawImageCanvasCommand.Source); var dest = ToSKRect(drawImageCanvasCommand.Dest); var paint = GetRenderPaint(drawImageCanvasCommand.Paint); - skCanvas.DrawImage(image, source, dest, paint); + var imagePaint = paint; + if (drawImageCanvasCommand.Sampling.HasValue) + { + imagePaint = paint?.Clone() ?? new SkiaSharp.SKPaint(); + imagePaint.FilterQuality = ToSKFilterQuality(drawImageCanvasCommand.Sampling.Value); + } + + try + { + skCanvas.DrawImage(image, source, dest, imagePaint); + } + finally + { + if (imagePaint is not null && !ReferenceEquals(imagePaint, paint)) + { + imagePaint.Dispose(); + } + } } } break; @@ -1896,7 +2020,15 @@ public void Draw(CanvasCommand canvasCommand, SkiaSharp.SKCanvas skCanvas, bool break; } - var textBlob = GetCachedPositionedTextBlob(drawPositionedTextCanvasCommand, paint); + using var font = drawPositionedTextCanvasCommand.TextBlob.Font is { } textBlobFont + ? ToSKFont(textBlobFont) + : ToSKFont(sourcePaint); + if (font is null) + { + break; + } + + var textBlob = GetCachedPositionedTextBlob(drawPositionedTextCanvasCommand, font); if (textBlob is not null) { skCanvas.DrawText(textBlob, 0, 0, paint); @@ -1919,9 +2051,29 @@ public void Draw(CanvasCommand canvasCommand, SkiaSharp.SKCanvas skCanvas, bool break; } - if (!TryDrawShapedText(skCanvas, text, x, y, paint)) + var textAlign = ToSKTextAlign(drawTextCanvasCommand.TextAlign ?? drawTextCanvasCommand.Paint.TextAlign); + var applyFont = drawTextCanvasCommand.Font is not null; + using var font = drawTextCanvasCommand.Font is { } textFont + ? ToSKFont(textFont) + : ToSKFont(drawTextCanvasCommand.Paint); + if (font is null) { - skCanvas.DrawText(text, x, y, paint); + break; + } + + var textPaint = applyFont || drawTextCanvasCommand.TextAlign.HasValue + ? CreateTextRenderPaint(paint, textAlign, font, applyFont) + : paint; + try + { + if (!TryDrawShapedText(skCanvas, text, x, y, textAlign, font, textPaint)) + { + skCanvas.DrawText(text, x, y, textPaint); + } + } + finally + { + DisposeIfCloned(textPaint, paint); } } break; @@ -1937,7 +2089,32 @@ public void Draw(CanvasCommand canvasCommand, SkiaSharp.SKCanvas skCanvas, bool var paint = wireframe ? ToWireframePaint(drawTextOnPathCanvasCommand.Paint) : GetRenderPaint(drawTextOnPathCanvasCommand.Paint); - skCanvas.DrawTextOnPath(text, path, hOffset, vOffset, paint); + if (path is null || paint is null) + { + break; + } + + var textAlign = ToSKTextAlign(drawTextOnPathCanvasCommand.TextAlign ?? drawTextOnPathCanvasCommand.Paint.TextAlign); + var applyFont = drawTextOnPathCanvasCommand.Font is not null; + using var font = drawTextOnPathCanvasCommand.Font is { } textFont + ? ToSKFont(textFont) + : ToSKFont(drawTextOnPathCanvasCommand.Paint); + if (font is null) + { + break; + } + + var textPaint = applyFont || drawTextOnPathCanvasCommand.TextAlign.HasValue + ? CreateTextRenderPaint(paint, textAlign, font, applyFont) + : paint; + try + { + skCanvas.DrawTextOnPath(text, path, hOffset, vOffset, textPaint); + } + finally + { + DisposeIfCloned(textPaint, paint); + } } break; } diff --git a/tests/Svg.Skia.UnitTests/SKSvgRebuildFromModelTests.cs b/tests/Svg.Skia.UnitTests/SKSvgRebuildFromModelTests.cs index ec402ada46..b665360376 100644 --- a/tests/Svg.Skia.UnitTests/SKSvgRebuildFromModelTests.cs +++ b/tests/Svg.Skia.UnitTests/SKSvgRebuildFromModelTests.cs @@ -325,6 +325,68 @@ public void ToSKPicture_ReflectsInPlaceMutatedImageDataAfterInitialNativeBuild() Assert.Equal(new SkiaColor(0, 0, 255, 255), rebuiltBitmap.GetPixel(0, 0)); } + [Fact] + public void ToSKPicture_UsesRecordedImageSampling() + { + var image = new ShimSkiaSharp.SKImage + { + Data = CreateEncodedBitmap(2, 1, static (x, _) => x == 0 + ? new SkiaColor(255, 0, 0, 255) + : new SkiaColor(0, 0, 255, 255)), + Width = 2, + Height = 1 + }; + var picture = new ShimSkiaSharp.SKPicture( + ShimSkiaSharp.SKRect.Create(0, 0, 20, 1), + new CanvasCommand[] + { + new DrawImageCanvasCommand( + image, + ShimSkiaSharp.SKRect.Create(0, 0, 2, 1), + ShimSkiaSharp.SKRect.Create(0, 0, 20, 1), + new ShimSkiaSharp.SKPaint { FilterQuality = ShimSkiaSharp.SKFilterQuality.None }, + new ShimSkiaSharp.SKSamplingOptions(ShimSkiaSharp.SKFilterMode.Linear, ShimSkiaSharp.SKMipmapMode.None)) + }); + var skiaModel = new SkiaModel(new SKSvgSettings()); + + using var renderedPicture = skiaModel.ToSKPicture(picture); + using var bitmap = RenderBitmap(Assert.IsType(renderedPicture)); + + var blendedPixels = CountPixels(bitmap, static color => color.Red > 0 && color.Blue > 0 && color.Green < 16); + Assert.True(blendedPixels > 0); + } + + [Fact] + public void ToSKPicture_UsesRecordedTextFontAndAlignment() + { + var paint = new ShimSkiaSharp.SKPaint + { + Color = new ShimSkiaSharp.SKColor(0, 0, 0, 255), + IsAntialias = false, + TextAlign = ShimSkiaSharp.SKTextAlign.Left, + TextSize = 8f + }; + var font = new ShimSkiaSharp.SKFont(null, 32f) + { + Edging = ShimSkiaSharp.SKFontEdging.Alias + }; + var picture = new ShimSkiaSharp.SKPicture( + ShimSkiaSharp.SKRect.Create(0, 0, 80, 45), + new CanvasCommand[] + { + new DrawTextCanvasCommand("MM", 40f, 34f, paint, ShimSkiaSharp.SKTextAlign.Right, font) + }); + var skiaModel = new SkiaModel(new SKSvgSettings()); + + using var renderedPicture = skiaModel.ToSKPicture(picture); + using var bitmap = RenderBitmap(Assert.IsType(renderedPicture)); + + var leftDarkPixels = CountPixels(bitmap, static (x, _, color) => x < 40 && IsDark(color)); + var rightDarkPixels = CountPixels(bitmap, static (x, _, color) => x >= 40 && IsDark(color)); + Assert.True(leftDarkPixels > 50); + Assert.True(leftDarkPixels > rightDarkPixels * 3); + } + [Fact] public void ToSKPicture_ReflectsInPlaceMutatedPolyPointsAfterInitialNativeBuild() { @@ -381,6 +443,45 @@ private static SkiaBitmap RenderBitmap(SkiaPicture picture) return Assert.IsType(bitmap); } + private static int CountPixels(SkiaBitmap bitmap, Func predicate) + { + var count = 0; + for (var y = 0; y < bitmap.Height; y++) + { + for (var x = 0; x < bitmap.Width; x++) + { + if (predicate(bitmap.GetPixel(x, y))) + { + count++; + } + } + } + + return count; + } + + private static int CountPixels(SkiaBitmap bitmap, Func predicate) + { + var count = 0; + for (var y = 0; y < bitmap.Height; y++) + { + for (var x = 0; x < bitmap.Width; x++) + { + if (predicate(x, y, bitmap.GetPixel(x, y))) + { + count++; + } + } + } + + return count; + } + + private static bool IsDark(SkiaColor color) + { + return color.Red < 128 && color.Green < 128 && color.Blue < 128; + } + private static void AssertTextPathCommandSource(CanvasCommand command) { Assert.Equal("path-text", command.SourceElementId); @@ -388,6 +489,22 @@ private static void AssertTextPathCommandSource(CanvasCommand command) Assert.False(string.IsNullOrWhiteSpace(command.SourceElementAddress)); } + private static byte[] CreateEncodedBitmap(int width, int height, Func getColor) + { + using var bitmap = new SkiaBitmap(width, height, SKColorType.Rgba8888, SKAlphaType.Unpremul); + for (var y = 0; y < height; y++) + { + for (var x = 0; x < width; x++) + { + bitmap.SetPixel(x, y, getColor(x, y)); + } + } + + using var image = SkiaSharp.SKImage.FromBitmap(bitmap); + using var data = image.Encode(SKEncodedImageFormat.Png, 100); + return data.ToArray(); + } + private static byte[] CreateEncodedSolidBitmap(SkiaColor color) { return new byte[] From 7b535163dcaa3bdb0a97d85bd62937798f1ba36a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Tue, 12 May 2026 18:35:19 +0200 Subject: [PATCH 5/6] Preserve exact runtime image sampling --- src/Svg.Skia/SkiaModel.cs | 177 +++++++++++++++++++++++++++++++++++++- 1 file changed, 175 insertions(+), 2 deletions(-) diff --git a/src/Svg.Skia/SkiaModel.cs b/src/Svg.Skia/SkiaModel.cs index 8c1661637a..162aa74199 100644 --- a/src/Svg.Skia/SkiaModel.cs +++ b/src/Svg.Skia/SkiaModel.cs @@ -1325,7 +1325,7 @@ public SkiaSharp.SKFilterQuality ToSKFilterQuality(SKFilterQuality filterQuality }; } - public SkiaSharp.SKFilterQuality ToSKFilterQuality(SKSamplingOptions samplingOptions) + private static SkiaSharp.SKFilterQuality ToLegacySKFilterQuality(SKSamplingOptions samplingOptions) { if (samplingOptions.UseCubic) { @@ -1342,6 +1342,23 @@ public SkiaSharp.SKFilterQuality ToSKFilterQuality(SKSamplingOptions samplingOpt : SkiaSharp.SKFilterQuality.Medium; } + private static bool TryDrawImageWithSamplingOptions( + SkiaSharp.SKCanvas skCanvas, + SkiaSharp.SKImage image, + SkiaSharp.SKRect source, + SkiaSharp.SKRect dest, + SKSamplingOptions samplingOptions, + SkiaSharp.SKPaint? paint) + { + if (!SkiaSharpSamplingOptionsApi.IsAvailable) + { + return false; + } + + SkiaSharpSamplingOptionsApi.DrawImage(skCanvas, image, source, dest, samplingOptions, paint); + return true; + } + private static SkiaSharp.SKPaint CreateTextRenderPaint( SkiaSharp.SKPaint paint, SkiaSharp.SKTextAlign textAlign, @@ -1950,14 +1967,32 @@ public void Draw(CanvasCommand canvasCommand, SkiaSharp.SKCanvas skCanvas, bool else { var image = GetRenderImage(drawImageCanvasCommand.Image); + if (image is null) + { + break; + } + var source = ToSKRect(drawImageCanvasCommand.Source); var dest = ToSKRect(drawImageCanvasCommand.Dest); var paint = GetRenderPaint(drawImageCanvasCommand.Paint); + var drewWithSampling = drawImageCanvasCommand.Sampling.HasValue && + TryDrawImageWithSamplingOptions( + skCanvas, + image, + source, + dest, + drawImageCanvasCommand.Sampling.Value, + paint); + if (drewWithSampling) + { + break; + } + var imagePaint = paint; if (drawImageCanvasCommand.Sampling.HasValue) { imagePaint = paint?.Clone() ?? new SkiaSharp.SKPaint(); - imagePaint.FilterQuality = ToSKFilterQuality(drawImageCanvasCommand.Sampling.Value); + imagePaint.FilterQuality = ToLegacySKFilterQuality(drawImageCanvasCommand.Sampling.Value); } try @@ -2208,4 +2243,142 @@ public void DrawWireframe(SKPicture picture, SkiaSharp.SKCanvas skCanvas) { Draw(picture, skCanvas, true); } + + private static class SkiaSharpSamplingOptionsApi + { +#if NET6_0_OR_GREATER + [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] +#endif + private static readonly Type? s_samplingOptionsType = Type.GetType("SkiaSharp.SKSamplingOptions, SkiaSharp", throwOnError: false); + + private static readonly Type? s_filterModeType = Type.GetType("SkiaSharp.SKFilterMode, SkiaSharp", throwOnError: false); + private static readonly Type? s_mipmapModeType = Type.GetType("SkiaSharp.SKMipmapMode, SkiaSharp", throwOnError: false); + +#if NET6_0_OR_GREATER + [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] +#endif + private static readonly Type? s_cubicResamplerType = Type.GetType("SkiaSharp.SKCubicResampler, SkiaSharp", throwOnError: false); + + private static readonly System.Reflection.ConstructorInfo? s_filterSamplingOptionsConstructor = GetConstructor(s_samplingOptionsType, s_filterModeType, s_mipmapModeType); + private static readonly System.Reflection.ConstructorInfo? s_cubicResamplerConstructor = GetConstructor(s_cubicResamplerType, typeof(float), typeof(float)); + private static readonly System.Reflection.ConstructorInfo? s_cubicSamplingOptionsConstructor = GetConstructor(s_samplingOptionsType, s_cubicResamplerType); + private static readonly System.Reflection.MethodInfo? s_drawImageWithSampling = GetDrawImageWithSampling(); + + public static bool IsAvailable => + s_samplingOptionsType is not null && + s_filterModeType is not null && + s_mipmapModeType is not null && + s_cubicResamplerType is not null && + s_filterSamplingOptionsConstructor is not null && + s_cubicResamplerConstructor is not null && + s_cubicSamplingOptionsConstructor is not null && + s_drawImageWithSampling is not null; + + public static void DrawImage( + SkiaSharp.SKCanvas skCanvas, + SkiaSharp.SKImage image, + SkiaSharp.SKRect source, + SkiaSharp.SKRect dest, + SKSamplingOptions samplingOptions, + SkiaSharp.SKPaint? paint) + { + var nativeSamplingOptions = ToNativeSamplingOptions(samplingOptions); + if (nativeSamplingOptions is null || s_drawImageWithSampling is null) + { + return; + } + + try + { + s_drawImageWithSampling.Invoke(skCanvas, new[] { image, source, dest, nativeSamplingOptions, paint }); + } + catch (System.Reflection.TargetInvocationException ex) when (ex.InnerException is not null) + { + System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture(ex.InnerException).Throw(); + } + } + + private static object? ToNativeSamplingOptions(SKSamplingOptions samplingOptions) + { + if (samplingOptions.UseCubic) + { + if (s_cubicResamplerType is null || s_cubicResamplerConstructor is null || s_cubicSamplingOptionsConstructor is null) + { + return null; + } + + var cubic = s_cubicResamplerConstructor.Invoke(new object[] { samplingOptions.Cubic.B, samplingOptions.Cubic.C }); + return s_cubicSamplingOptionsConstructor.Invoke(new[] { cubic }); + } + + if (s_filterModeType is null || s_mipmapModeType is null || s_filterSamplingOptionsConstructor is null) + { + return null; + } + + var filterMode = Enum.ToObject(s_filterModeType, (int)samplingOptions.Filter); + var mipmapMode = Enum.ToObject(s_mipmapModeType, (int)samplingOptions.Mipmap); + return s_filterSamplingOptionsConstructor.Invoke(new[] { filterMode, mipmapMode }); + } + + private static System.Reflection.ConstructorInfo? GetConstructor( +#if NET6_0_OR_GREATER + [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] Type? type, +#else + Type? type, +#endif + params Type?[] parameterTypes) + { + if (type is null) + { + return null; + } + + var resolvedParameterTypes = new Type[parameterTypes.Length]; + for (var i = 0; i < parameterTypes.Length; i++) + { + if (parameterTypes[i] is not { } parameterType) + { + return null; + } + + resolvedParameterTypes[i] = parameterType; + } + + return type.GetConstructor(resolvedParameterTypes); + } + + private static System.Reflection.MethodInfo? GetDrawImageWithSampling() + { + if (s_samplingOptionsType is null) + { + return null; + } + + var methods = typeof(SkiaSharp.SKCanvas).GetMethods( + System.Reflection.BindingFlags.Instance | + System.Reflection.BindingFlags.Public); + for (var i = 0; i < methods.Length; i++) + { + var method = methods[i]; + if (method.Name != nameof(SkiaSharp.SKCanvas.DrawImage)) + { + continue; + } + + var parameters = method.GetParameters(); + if (parameters.Length == 5 && + parameters[0].ParameterType == typeof(SkiaSharp.SKImage) && + parameters[1].ParameterType == typeof(SkiaSharp.SKRect) && + parameters[2].ParameterType == typeof(SkiaSharp.SKRect) && + parameters[3].ParameterType == s_samplingOptionsType && + parameters[4].ParameterType == typeof(SkiaSharp.SKPaint)) + { + return method; + } + } + + return null; + } + } } From d52959951bac927cf3504ae7259ec58bef4036ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Tue, 12 May 2026 19:49:01 +0200 Subject: [PATCH 6/6] Preserve aliased paint text fonts --- .../SkiaCSharpModelExtensions.cs | 6 +++++- src/Svg.Skia/SkiaModel.cs | 14 ++++++++++++- .../SKSvgRebuildFromModelTests.cs | 14 +++++++++++++ .../SkiaCSharpCodeGenTests.cs | 20 +++++++++++++++++++ 4 files changed, 52 insertions(+), 2 deletions(-) diff --git a/src/Svg.CodeGen.Skia/SkiaCSharpModelExtensions.cs b/src/Svg.CodeGen.Skia/SkiaCSharpModelExtensions.cs index 82fc41103c..e24e3b08b8 100644 --- a/src/Svg.CodeGen.Skia/SkiaCSharpModelExtensions.cs +++ b/src/Svg.CodeGen.Skia/SkiaCSharpModelExtensions.cs @@ -1520,7 +1520,11 @@ public static void ToSKFont(this SKPaint paint, SkiaCSharpCodeGenCounter counter sb.AppendLine($"{indent}var {counter.FontVarName}{counterFont} = new SKFont({typefaceExpression}, {paint.TextSize.ToFloatString()});"); - if (paint.LcdRenderText != false) + if (!paint.IsAntialias) + { + sb.AppendLine($"{indent}{counter.FontVarName}{counterFont}.Edging = SKFontEdging.Alias;"); + } + else if (paint.LcdRenderText != false) { sb.AppendLine($"{indent}{counter.FontVarName}{counterFont}.Edging = SKFontEdging.SubpixelAntialias;"); } diff --git a/src/Svg.Skia/SkiaModel.cs b/src/Svg.Skia/SkiaModel.cs index 162aa74199..7776a6fead 100644 --- a/src/Svg.Skia/SkiaModel.cs +++ b/src/Svg.Skia/SkiaModel.cs @@ -164,6 +164,18 @@ public SkiaSharp.SKFontEdging ToSKFontEdging(SKFontEdging edging) }; } + private static SkiaSharp.SKFontEdging ToSKFontEdging(SKPaint paint) + { + if (!paint.IsAntialias) + { + return SkiaSharp.SKFontEdging.Alias; + } + + return paint.LcdRenderText + ? SkiaSharp.SKFontEdging.SubpixelAntialias + : SkiaSharp.SKFontEdging.Antialias; + } + public SkiaSharp.SKFontStyleWeight ToSKFontStyleWeight(SKFontStyleWeight fontStyleWeight) { return fontStyleWeight switch @@ -1401,7 +1413,7 @@ public SkiaSharp.SKFont ToSKFont(SKPaint paint) var typefaceResolution = ResolveSKTypeface(paint.Typeface); var skFont = new SkiaSharp.SKFont(typefaceResolution.Typeface, paint.TextSize) { - Edging = paint.LcdRenderText ? SkiaSharp.SKFontEdging.SubpixelAntialias : SkiaSharp.SKFontEdging.Antialias, + Edging = ToSKFontEdging(paint), Subpixel = paint.SubpixelText }; diff --git a/tests/Svg.Skia.UnitTests/SKSvgRebuildFromModelTests.cs b/tests/Svg.Skia.UnitTests/SKSvgRebuildFromModelTests.cs index b665360376..42aecd5133 100644 --- a/tests/Svg.Skia.UnitTests/SKSvgRebuildFromModelTests.cs +++ b/tests/Svg.Skia.UnitTests/SKSvgRebuildFromModelTests.cs @@ -387,6 +387,20 @@ public void ToSKPicture_UsesRecordedTextFontAndAlignment() Assert.True(leftDarkPixels > rightDarkPixels * 3); } + [Fact] + public void ToSKFont_MapsAliasedPaintToAliasEdging() + { + var skiaModel = new SkiaModel(new SKSvgSettings()); + + using var font = skiaModel.ToSKFont(new ShimSkiaSharp.SKPaint + { + IsAntialias = false, + LcdRenderText = true + }); + + Assert.Equal(SkiaSharp.SKFontEdging.Alias, font.Edging); + } + [Fact] public void ToSKPicture_ReflectsInPlaceMutatedPolyPointsAfterInitialNativeBuild() { diff --git a/tests/Svg.Skia.UnitTests/SkiaCSharpCodeGenTests.cs b/tests/Svg.Skia.UnitTests/SkiaCSharpCodeGenTests.cs index 359293db14..aec426db3b 100644 --- a/tests/Svg.Skia.UnitTests/SkiaCSharpCodeGenTests.cs +++ b/tests/Svg.Skia.UnitTests/SkiaCSharpCodeGenTests.cs @@ -158,6 +158,26 @@ public void Generate_UsesTextBlobFontForPositionedText() Assert.DoesNotContain(".ToFont()", code); } + [Fact] + public void Generate_UsesAliasFontEdgingForAliasedTextPaint() + { + var paint = new SKPaint + { + IsAntialias = false, + LcdRenderText = true, + TextSize = 18f + }; + var picture = new SKPicture(SKRect.Create(0f, 0f, 40f, 20f), new List + { + new DrawTextCanvasCommand("Text", 1f, 18f, paint) + }); + + var code = SkiaCSharpCodeGen.Generate(picture, "Svg", "Generated"); + + Assert.Contains(".Edging = SKFontEdging.Alias;", code); + Assert.DoesNotContain(".Edging = SKFontEdging.SubpixelAntialias;", code); + } + [Fact] public void SkiaModel_ToSKShader_CreatesGradientsWithOptionalColorPositions() {