将 UWP canvas 导出到 SVG

Export UWP canvas to SVG

我正在尝试从在 XAML canvas 控件上绘制的某些形状对象(路径几何、椭圆等)创建 SVG 文件(canvases 呈现在在网格控件内彼此顶部)。看起来 Win2D 可以提供 classes 来生成 SVG 文件,但我正在努力弄清楚如何用形状填充 CanvasSvgDocument class。

This 是我找到的唯一部分示例,但答案似乎包括对 XML 字符串的转换以加载到 CanvasSvgDocument 中,这似乎是在执行相同的任务两次(如SVG 文件是 XML)。有谁能举例说明我该怎么做吗?

我目前对结果代码的最佳猜测是:

using Microsoft.Graphics.Canvas;
using Microsoft.Graphics.Canvas.Svg;
using Microsoft.Graphics.Canvas.UI.Xaml;
using System;
using System.Threading.Tasks;
using Windows.Storage.Streams;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Shapes;

namespace MyApp
{
    public class ExportSVG
    {
        private CanvasSvgDocument SVG { get; } = new(new CanvasDevice());

        public async Task SaveASync(IRandomAccessStream stream) => await SVG.SaveAsync(stream);

        public void AddCanvases(UIElement element)
        {
            if (element is Grid grid)
            {
                foreach (UIElement child in grid.Children)
                {
                    AddCanvases(child);
                }
            }
            else if (element is Canvas canvas)
            {
                AddCanvas(canvas);
            }
        }

        public void AddCanvas(Canvas canvas)
        {
            foreach (UIElement element in canvas.Children)
            {
                if (element is Path path)
                {
                    if (path.Data is PathGeometry pathGeometry)
                    {
                        foreach (PathFigure pathFigure in pathGeometry.Figures)
                        {
                            // Add path to SVG
                        }
                    }
                    else if (path.Data is EllipseGeometry ellipseGeometry)
                    {
                        // Add ellipse to SVG
                    }
                }
                else if (element is TextBlock textBlock)
                {
                    // add text to SVG
                }
            }
        }
    }
}

可以使用CanvasGeometry.CreateInk将笔划墨迹转化为几何图形,使用CanvasGeometry命名空间下的相关方法获取路径,然后自定义class读取解析路径。最后生成的CanvasSvgDocument对象用于保存包含svg内容的流

请参考以下示例来执行这些步骤。 (注:下载Win2D.uwp包)

XAML代码:

<Page
    x:Class="CanvasToSVG.MainPage"
    …
   mc:Ignorable="d"
    Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">

    <Grid>
        <StackPanel>
            <InkCanvas x:Name="MyInkConntrol" Height="500">
               
            </InkCanvas>
            <InkToolbar Grid.Row="1" TargetInkCanvas="{x:Bind MyInkConntrol}" HorizontalAlignment="Left">
                <InkToolbarCustomToolButton Click="save">
                    <SymbolIcon Symbol="Save" />
                </InkToolbarCustomToolButton>
            </InkToolbar>
            <Line Stroke="Black"/>
            <Image Name="ImageControl"></Image>
        </StackPanel>
    </Grid>
</Page>

后面的代码:

 public sealed partial class MainPage : Page
    {
        public MainPage()
        {
            this.InitializeComponent();
            MyInkConntrol.InkPresenter.InputDeviceTypes= CoreInputDeviceTypes.Mouse |CoreInputDeviceTypes.Pen |
                                                       CoreInputDeviceTypes.Touch;

            MyInkConntrol.InkPresenter.StrokesCollected += InkPresenter_StrokesCollected;
            MyInkConntrol.InkPresenter.StrokesErased += InkPresenter_StrokesErased;
        }

        private async void InkPresenter_StrokesErased(Windows.UI.Input.Inking.InkPresenter sender, Windows.UI.Input.Inking.InkStrokesErasedEventArgs args)
        {
            await RenderSvg();
        }

        private async void InkPresenter_StrokesCollected(Windows.UI.Input.Inking.InkPresenter sender, Windows.UI.Input.Inking.InkStrokesCollectedEventArgs args)
        {
            await RenderSvg();
        }
        public async Task RenderSvg()
        {
            using (var stream=new InMemoryRandomAccessStream())
            {
                await RenderSvg(stream);
                var image= new SvgImageSource();
                await image.SetSourceAsync(stream);
                ImageControl.Source = image;
            }
        }
       
        public async Task RenderSvg(IRandomAccessStream randomAccessStream)
        {
            var sharedDevice = CanvasDevice.GetSharedDevice();
            using (var offscreen = new CanvasRenderTarget(sharedDevice, (float)MyInkConntrol.RenderSize.Width, (float)MyInkConntrol.RenderSize.Height, 96))
            {
                using (var session = offscreen.CreateDrawingSession())
                {
                    var svgDocument = new CanvasSvgDocument(sharedDevice);

                    svgDocument.Root.SetStringAttribute("viewBox", $"0 0 {MyInkConntrol.RenderSize.Width} {MyInkConntrol.RenderSize.Height}");

                    foreach (var stroke in MyInkConntrol.InkPresenter.StrokeContainer.GetStrokes())
                    {
                        var canvasGeometry = CanvasGeometry.CreateInk(session, new[] { stroke }).Outline();

                        var pathReceiver = new CanvasGeometryToSvgPathReader();
                        canvasGeometry.SendPathTo(pathReceiver);
                        var element = svgDocument.Root.CreateAndAppendNamedChildElement("path");
                        element.SetStringAttribute("d", pathReceiver.Path);
                        var color = stroke.DrawingAttributes.Color;
                        element.SetColorAttribute("fill", color);

                    }

                   await svgDocument.SaveAsync(randomAccessStream);
                }

            }
        }

        private async void save(object sender, RoutedEventArgs e)
        {
            FileSavePicker savePicker = new FileSavePicker();
            savePicker.SuggestedStartLocation = PickerLocationId.DocumentsLibrary;
            savePicker.FileTypeChoices.Add("svg file", new List<string>() { ".svg" });
            savePicker.SuggestedFileName = "NewSvgfile1";
            var file = await savePicker.PickSaveFileAsync();
            if (file != null)
            {
                using (var writeStream = (await file.OpenStreamForWriteAsync()).AsRandomAccessStream())
                {
                    await RenderSvg(writeStream);
                    await writeStream.FlushAsync();
                }
            }
        }
    }

自定义 class:

public class CanvasGeometryToSvgPathReader: ICanvasPathReceiver
{
    private readonly Vector2 _ratio;
    private List<string> Parts { get; }
    public string Path => string.Join(" ", Parts);
    public CanvasGeometryToSvgPathReader() : this(Vector2.One)
    { }

    public CanvasGeometryToSvgPathReader(Vector2 ratio)
    {
        _ratio = ratio;
        Parts = new List<string>();
    }

    public void BeginFigure(Vector2 startPoint, CanvasFigureFill figureFill)
    {
        Parts.Add($"M{startPoint.X / _ratio.X} {startPoint.Y / _ratio.Y}");
    }

    public void AddArc(Vector2 endPoint, float radiusX, float radiusY, float rotationAngle, CanvasSweepDirection sweepDirection, CanvasArcSize arcSize)
    {
      
    }

    public void AddCubicBezier(Vector2 controlPoint1, Vector2 controlPoint2, Vector2 endPoint)
    {
        Parts.Add($"C{controlPoint1.X / _ratio.X},{controlPoint1.Y / _ratio.Y} {controlPoint2.X / _ratio.X},{controlPoint2.Y / _ratio.Y} {endPoint.X / _ratio.X},{endPoint.Y / _ratio.Y}");
    }

    public void AddLine(Vector2 endPoint)
    {
        Parts.Add($"L {endPoint.X / _ratio.X} {endPoint.Y / _ratio.Y}");
    }

    public void AddQuadraticBezier(Vector2 controlPoint, Vector2 endPoint)
    {
        //
    }

    public void SetFilledRegionDetermination(CanvasFilledRegionDetermination filledRegionDetermination)
    {
       //
    }

    public void SetSegmentOptions(CanvasFigureSegmentOptions figureSegmentOptions)
    {
        //
    }

    public void EndFigure(CanvasFigureLoop figureLoop)
    {
        Parts.Add("Z");
    }
}

最后我能够使用 XmlWriter class 编写我自己的 canvas-to-svg 转换器。使用问题中的示例:

using Microsoft.Graphics.Canvas;
using Microsoft.Graphics.Canvas.Svg;
using Microsoft.Graphics.Canvas.UI.Xaml;
using System;
using System.Xml;
using System.Threading.Tasks;
using Windows.Storage.Streams;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Shapes;

namespace MyApp
{
    public class ExportSVG
    {
        private XmlWriter Writer { get; }
        public SVGWriter(System.IO.Stream stream)
        {
            Writer = XmlWriter.Create(stream, new XmlWriterSettings()
            {
                Indent = true,
            });
            Writer.WriteStartElement("svg", "http://www.w3.org/2000/svg");
            Write("version", "1.1");
        }

        public void AddCanvases(UIElement element)
        {
            if (element is Grid grid)
            {
                foreach (UIElement child in grid.Children)
                {
                    AddCanvases(child);
                }
            }
            else if (element is Canvas canvas)
            {
                AddCanvas(canvas);
            }
        }

        public void AddCanvas(Canvas canvas)
        {
            foreach (UIElement element in canvas.Children)
            {
                if (element is Path path)
                {
                    else if (path.Data is EllipseGeometry ellipseGeometry)
                    {
                        Writer.WriteStartElement("ellipse");
                        Write("stroke", ellipseGeometry.Stroke);
                        Write("stroke-width", ellipseGeometry.StrokeThickness);
                        Write("cx", ellipseGeometry.Center.X);
                        Write("cy", ellipseGeometry.Center.Y);
                        Write("rx", ellipseGeometry.RadiusX);
                        Write("ry", ellipseGeometry.RadiusY);
                        Writer.WriteEndElement();
                    }
                }
                else if (element is TextBlock textBlock)
                {
                    Writer.WriteStartElement("text");
                    Write("x", Canvas.GetLeft(textBlock));
                    Write("y", Canvas.GetTop(textBlock) + textBlock.ActualHeight);
                    Write("font-family", textBlock.FontFamily.Source);
                    Write("font-size", $"{textBlock.FontSize}px");
                    Writer.WriteString(textBlock.Text);
                    Writer.WriteEndElement();
                }
            }
        }

        private void Write(string name, string value)
        {
            Writer.WriteAttributeString(name, value);
        }

        private void Write(string name, double value)
        {
            Write(name, ((float)value).ToString());
        }

        public void Dispose()
        {
            Writer.WriteEndElement();
            Writer.Close();
            Writer.Dispose();
        }
    }
}