问题描述
我正在尝试从在 XAML 画布控件上绘制的一些形状对象(路径几何图形、椭圆等)创建一个 SVG 文件(画布在网格控件中彼此重叠呈现)。看起来 Win2D 可以提供 classes 来生成 SVG 文件,但我正在努力弄清楚如何用形状填充 CanvasSvgDocument 类。
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 命名空间下的相关方法获取路径,然后编写自定义类读取解析路径。最后,生成的 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();
}
}
}
}
自定义类:
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 类来编写我自己的画布到 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();
}
}
}