2

I'm trying to create an SVG file from some shape objects(path geometries, ellipses etc.) drawn on XAML canvas controls (the canvases are rendered on top of each other inside grid controls). It looks like Win2D can provide the classes to generate the SVG file, but I'm struggling to figure out how to populate the CanvasSvgDocument class with the shapes.

This is the only partial example I've found, but the answer seems to include a conversion to XML strings to load into the CanvasSvgDocument which seems like doing the same task twice (as SVG files are XML). Is anybody able to provide an example of how I might do this?

My current best guess of what the resulting code might look like is:

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
                }
            }
        }
    }
}
Siyh
  • 1,747
  • 1
  • 17
  • 24
  • An SVG file is XML format. There are two separate steps 1) Generate the SVG file 2) Display the results. It looks like you are trying to combine the two steps into one step. A XAML is a VS project to create and display a form which can be used to display the SVG. A CanvasSvgDocument I'm not sure if it is the XML file or the Way the SVG is being displayed. – jdweng Mar 14 '21 at 15:45
  • @jdweng Let my clarify: I'm just trying to generate the SVG file, but the SVG should contain what the XAML controls in my app currently show on screen. – Siyh Mar 15 '21 at 08:14
  • 1
    You can use any of the XML libraries in Net to generate the file. SVG has layers and you have to define the layers and the format of the xml. – jdweng Mar 15 '21 at 09:30

2 Answers2

1

You could use CanvasGeometry.CreateInk convert the ink strokes into geometry and use the relevant methods under the CanvasGeometry namespace to get the path, then write a custom class to read the parse path. Finally, the generated CanvasSvgDocument object is used to save the stream containing the svg content.

Please refer to the following sample to do these steps. (Note: download Win2D.uwp package)

XAML code:

<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>

Code behind:

 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();
                }
            }
        }
    }

Custom 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");
    }
}
dear_vv
  • 2,350
  • 1
  • 4
  • 13
0

In the end I was able to use the XmlWriter class to write my own canvas-to-svg converter. Using the example in the question:

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();
        }
    }
}
Siyh
  • 1,747
  • 1
  • 17
  • 24