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
27 changes: 21 additions & 6 deletions src/ShimSkiaSharp/SKCanvas.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)),
Expand Down Expand Up @@ -65,17 +65,17 @@ 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;

public record DrawPathCanvasCommand(SKPath? Path, SKPaint? Paint) : CanvasCommand;

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;

Expand Down Expand Up @@ -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));
Comment on lines +209 to +211

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor sampling when replaying image commands

When a caller records an image with the new DrawImage(..., SKSamplingOptions, ...) overload, the retained model stores Sampling, but SkiaModel.Draw still renders every DrawImageCanvasCommand via the old skCanvas.DrawImage(image, source, dest, paint) path and never reads drawImageCanvasCommand.Sampling (checked src/Svg.Skia/SkiaModel.cs lines 1845-1849). As a result, models that request cubic/linear sampling display with whatever the paint/default legacy path chooses, while codegen emits the requested sampling, so runtime rendering and generated output diverge.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 568a1081a9bbc4720aecaa0b3507ef49330aa46f.

SkiaModel.Draw now reads DrawImageCanvasCommand.Sampling during retained replay. Because Svg.Skia currently compiles against a SkiaSharp runtime surface where SKCanvas.DrawImage(..., SKSamplingOptions, ...) is not available, the replay path translates the retained shim SKSamplingOptions to the closest runtime SKFilterQuality and applies it to a scoped cloned SKPaint. This keeps cached paints immutable and makes retained runtime rendering honor explicit recorded sampling instead of falling back to the legacy paint/default choice.

I also added SKSvgRebuildFromModelTests.ToSKPicture_UsesRecordedImageSampling, which renders a retained two-color image with explicit linear sampling and verifies blended pixels are produced.

Validation run after the fix:

  • dotnet format Svg.Skia.slnx --no-restore
  • dotnet build Svg.Skia.slnx -c Release --no-restore -v:minimal /nologo
  • dotnet test Svg.Skia.slnx -c Release --no-restore -v:minimal /nologo

The full build passed with existing unrelated warnings only, and the full test run passed. Svg.Skia.UnitTests now reports 1848 passed and 533 skipped.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additional refinement landed in 7b535163dcaa3bdb0a97d85bd62937798f1ba36a.

The initial fix in 568a1081a9bbc4720aecaa0b3507ef49330aa46f made retained replay consume DrawImageCanvasCommand.Sampling. This follow-up removes the remaining lossy behavior for newer SkiaSharp runtimes: when SKSamplingOptions and the matching SKCanvas.DrawImage overload are present at runtime, replay now constructs the native sampling options and invokes that exact overload. The SKFilterQuality mapping remains only as the SkiaSharp 2.88 fallback path.

This was validated with the focused retained replay tests plus the full release build and full release test suite after the follow-up commit.

}

public void DrawPicture(SKPicture picture)
{
AddCommand(new DrawPictureCanvasCommand(picture));
Expand All @@ -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));
Comment on lines +234 to +236

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor recorded SKFont when replaying text

The new overload records TextAlign and SKFont, but the renderer does not consume those fields: SkiaModel.Draw still calls TryDrawShapedText(..., paint) / skCanvas.DrawText(text, x, y, paint) for DrawTextCanvasCommand (checked src/Svg.Skia/SkiaModel.cs lines 1922-1924), and the same applies to the old path-based draw call. Any retained model created through this overload with a font size/typeface/scale or alignment that differs from the paint will render incorrectly even though codegen preserves the font.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 568a1081a9bbc4720aecaa0b3507ef49330aa46f.

Retained text replay now consumes the recorded font and alignment state instead of relying only on the paint. The fix adds runtime SKFont conversion for shim SKPaint and shim SKFont, including typeface resolution, size, scale, skew, edging, subpixel, and embolden state. TryDrawShapedText now shapes with the selected SKFont and uses the selected SKTextAlign for shaped-text alignment.

For fallback/runtime APIs that still draw text through SKPaint, SkiaModel.Draw creates a scoped cloned paint when the retained command provides an explicit font or alignment, applies those fields to that clone, and leaves cached render paints untouched. The same pattern is applied to DrawTextOnPathCanvasCommand. Positioned text replay now also keys its blob cache by SKFont signature and uses SKTextBlob.Font when the retained blob carries an explicit font.

I added SKSvgRebuildFromModelTests.ToSKPicture_UsesRecordedTextFontAndAlignment, which renders a retained text command whose recorded font/alignment intentionally differs from the paint and verifies the rendered output lands on the expected side with the larger font.

Validation run after the fix:

  • dotnet format Svg.Skia.slnx --no-restore
  • dotnet build Svg.Skia.slnx -c Release --no-restore -v:minimal /nologo
  • dotnet test Svg.Skia.slnx -c Release --no-restore -v:minimal /nologo

The full build passed with existing unrelated warnings only, and the full test run passed. Svg.Skia.UnitTests now reports 1848 passed and 533 skipped.

}

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);
Expand Down
174 changes: 174 additions & 0 deletions src/ShimSkiaSharp/SKFont.cs
Original file line number Diff line number Diff line change
@@ -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<SKFont>
{
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;
}
}
62 changes: 62 additions & 0 deletions src/ShimSkiaSharp/SKSamplingOptions.cs
Original file line number Diff line number Diff line change
@@ -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; }
}
12 changes: 12 additions & 0 deletions src/ShimSkiaSharp/SKTextBlob.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ public sealed class SKTextBlob : ICloneable, IDeepCloneable<SKTextBlob>
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()
{
Expand All @@ -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 };

Expand All @@ -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;
}
Expand Down
Loading
Loading